...

Source file src/github.com/GoogleCloudPlatform/k8s-config-connector/pkg/crd/crdgeneration/dcl2crdgeneration_test.go

Documentation: github.com/GoogleCloudPlatform/k8s-config-connector/pkg/crd/crdgeneration

     1  // Copyright 2022 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package crdgeneration
    16  
    17  import (
    18  	"testing"
    19  
    20  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/crd/crdgeneration/crdboilerplate"
    21  	dclmetadata "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/metadata"
    22  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/gvks/supportedgvks"
    23  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/servicemapping/servicemappingloader"
    24  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/test"
    25  	testdclschemaloader "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/test/dclschemaloader"
    26  	testservicemetadataloader "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/test/servicemetadataloader"
    27  
    28  	"github.com/google/go-cmp/cmp"
    29  	"github.com/nasa9084/go-openapi"
    30  	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    31  )
    32  
    33  func TestDCLSchemaToJSONSchema(t *testing.T) {
    34  	tests := []struct {
    35  		name                     string
    36  		dclSchema                *openapi.Schema
    37  		specSchema               *apiextensions.JSONSchemaProps
    38  		statusSchema             *apiextensions.JSONSchemaProps
    39  		resource                 dclmetadata.Resource
    40  		hasErrorOnSpecGeneration bool
    41  	}{
    42  		{
    43  			name: "primitive fields",
    44  			dclSchema: &openapi.Schema{
    45  				Type: "object",
    46  				Properties: map[string]*openapi.Schema{
    47  					"foo": {
    48  						Type: "string",
    49  					},
    50  					"bar": {
    51  						Type: "integer",
    52  					},
    53  					"baz": {
    54  						Type: "boolean",
    55  					},
    56  					"quz": {
    57  						Type: "number",
    58  					},
    59  				},
    60  				Required: []string{"foo"},
    61  			},
    62  			specSchema: &apiextensions.JSONSchemaProps{
    63  				Type: "object",
    64  				Properties: map[string]apiextensions.JSONSchemaProps{
    65  					"foo": {
    66  						Type: "string",
    67  					},
    68  					"bar": {
    69  						Type: "integer",
    70  					},
    71  					"baz": {
    72  						Type: "boolean",
    73  					},
    74  					"quz": {
    75  						Type: "number",
    76  					},
    77  				},
    78  				Required: []string{"foo"},
    79  			},
    80  			statusSchema: nil,
    81  		},
    82  		{
    83  			name: "primitive fields with read-only fields",
    84  			dclSchema: &openapi.Schema{
    85  				Type: "object",
    86  				Properties: map[string]*openapi.Schema{
    87  					"foo": {
    88  						Type: "string",
    89  					},
    90  					"bar": {
    91  						Type:     "integer",
    92  						ReadOnly: true,
    93  					},
    94  				},
    95  				Required: []string{"foo"},
    96  			},
    97  			specSchema: &apiextensions.JSONSchemaProps{
    98  				Type: "object",
    99  				Properties: map[string]apiextensions.JSONSchemaProps{
   100  					"foo": {
   101  						Type: "string",
   102  					},
   103  				},
   104  				Required: []string{"foo"},
   105  			},
   106  			statusSchema: &apiextensions.JSONSchemaProps{
   107  				Type: "object",
   108  				Properties: map[string]apiextensions.JSONSchemaProps{
   109  					"bar": {
   110  						Type: "integer",
   111  					},
   112  				},
   113  			},
   114  		},
   115  		{
   116  			name: "nested fields with read-only fields",
   117  			dclSchema: &openapi.Schema{
   118  				Type: "object",
   119  				Properties: map[string]*openapi.Schema{
   120  					"foo": {
   121  						Type: "object",
   122  						Properties: map[string]*openapi.Schema{
   123  							"nestedField1": {
   124  								Type: "boolean",
   125  							},
   126  							"nestedField2": {
   127  								Type:     "string",
   128  								ReadOnly: true,
   129  							},
   130  						},
   131  						Required: []string{"nestedField1"},
   132  					},
   133  					"bar": {
   134  						Type: "integer",
   135  					},
   136  				},
   137  				Required: []string{"foo"},
   138  			},
   139  			specSchema: &apiextensions.JSONSchemaProps{
   140  				Type: "object",
   141  				Properties: map[string]apiextensions.JSONSchemaProps{
   142  					"foo": {
   143  						Type: "object",
   144  						Properties: map[string]apiextensions.JSONSchemaProps{
   145  							"nestedField1": {
   146  								Type: "boolean",
   147  							},
   148  						},
   149  						Required: []string{"nestedField1"},
   150  					},
   151  					"bar": {
   152  						Type: "integer",
   153  					},
   154  				},
   155  				Required: []string{"foo"},
   156  			},
   157  			statusSchema: &apiextensions.JSONSchemaProps{
   158  				Type: "object",
   159  				Properties: map[string]apiextensions.JSONSchemaProps{
   160  					"foo": {
   161  						Type: "object",
   162  						Properties: map[string]apiextensions.JSONSchemaProps{
   163  							"nestedField2": {
   164  								Type: "string",
   165  							},
   166  						},
   167  					},
   168  				},
   169  			},
   170  		},
   171  		{
   172  			name: "sensitive field",
   173  			dclSchema: &openapi.Schema{
   174  				Type: "object",
   175  				Properties: map[string]*openapi.Schema{
   176  					"foo": {
   177  						Type: "string",
   178  						Extension: map[string]interface{}{
   179  							"x-dcl-sensitive": true,
   180  						},
   181  					},
   182  					"bar": {
   183  						Type: "integer",
   184  					},
   185  					// read-only sensitive fields will NOT be converted to secret references
   186  					"baz": {
   187  						Type:     "string",
   188  						ReadOnly: true,
   189  						Extension: map[string]interface{}{
   190  							"x-dcl-sensitive": true,
   191  						},
   192  					},
   193  				},
   194  				Required: []string{"foo"},
   195  			},
   196  			specSchema: &apiextensions.JSONSchemaProps{
   197  				Type: "object",
   198  				Properties: map[string]apiextensions.JSONSchemaProps{
   199  					"foo": crdboilerplate.GetSensitiveFieldSchemaBoilerplate(),
   200  					"bar": {
   201  						Type: "integer",
   202  					},
   203  				},
   204  				Required: []string{"foo"},
   205  			},
   206  			statusSchema: &apiextensions.JSONSchemaProps{
   207  				Type: "object",
   208  				Properties: map[string]apiextensions.JSONSchemaProps{
   209  					"baz": {
   210  						Type: "string",
   211  					},
   212  				},
   213  			},
   214  		},
   215  		{
   216  			name: "reference field",
   217  			dclSchema: &openapi.Schema{
   218  				Type: "object",
   219  				Properties: map[string]*openapi.Schema{
   220  					"foo": {
   221  						Type: "string",
   222  						Extension: map[string]interface{}{
   223  							"x-dcl-references": []interface{}{
   224  								map[interface{}]interface{}{
   225  									"resource": "Test1/Foo",
   226  									"field":    "name",
   227  								},
   228  							},
   229  						},
   230  					},
   231  					"bar": {
   232  						Type: "integer",
   233  					},
   234  					// read-only reference fields will NOT be converted to resource references
   235  					"baz": {
   236  						Type:     "string",
   237  						ReadOnly: true,
   238  						Extension: map[string]interface{}{
   239  							"x-dcl-references": []interface{}{
   240  								map[interface{}]interface{}{
   241  									"resource": "FakeService/FakeKind",
   242  									"field":    "name",
   243  								},
   244  							},
   245  						},
   246  					},
   247  				},
   248  				Required: []string{"foo"},
   249  			},
   250  			specSchema: &apiextensions.JSONSchemaProps{
   251  				Type: "object",
   252  				Properties: map[string]apiextensions.JSONSchemaProps{
   253  					"fooRef": *crdboilerplate.GetResourceReferenceSchemaBoilerplate(
   254  						"Allowed value: The Google Cloud resource name of a `Test1Foo` resource (format: `projects/{{project}}/foo/{{name}}`).",
   255  					),
   256  					"bar": {
   257  						Type: "integer",
   258  					},
   259  				},
   260  				Required: []string{"fooRef"},
   261  			},
   262  			statusSchema: &apiextensions.JSONSchemaProps{
   263  				Type: "object",
   264  				Properties: map[string]apiextensions.JSONSchemaProps{
   265  					"baz": {
   266  						Type: "string",
   267  					},
   268  				},
   269  			},
   270  		},
   271  		{
   272  			name: "reference nested in object",
   273  			dclSchema: &openapi.Schema{
   274  				Type: "object",
   275  				Properties: map[string]*openapi.Schema{
   276  					"foo": {
   277  						Type: "object",
   278  						Properties: map[string]*openapi.Schema{
   279  							"bar": {
   280  								Type: "string",
   281  								Extension: map[string]interface{}{
   282  									"x-dcl-references": []interface{}{
   283  										map[interface{}]interface{}{
   284  											"resource": "Test1/Bar",
   285  											"field":    "name",
   286  										},
   287  									},
   288  								},
   289  							},
   290  							"baz": {
   291  								Type: "integer",
   292  							},
   293  						},
   294  					},
   295  				},
   296  			},
   297  			specSchema: &apiextensions.JSONSchemaProps{
   298  				Type: "object",
   299  				Properties: map[string]apiextensions.JSONSchemaProps{
   300  					"foo": {
   301  						Type: "object",
   302  						Properties: map[string]apiextensions.JSONSchemaProps{
   303  							"barRef": *crdboilerplate.GetResourceReferenceSchemaBoilerplate(
   304  								"Allowed value: The Google Cloud resource name of a `Test1Bar` resource (format: `projects/{{project}}/bar/{{name}}`).",
   305  							),
   306  							"baz": {
   307  								Type: "integer",
   308  							},
   309  						},
   310  					},
   311  				},
   312  			},
   313  			statusSchema: nil,
   314  		},
   315  		{
   316  			name: "a list of reference",
   317  			dclSchema: &openapi.Schema{
   318  				Type: "object",
   319  				Properties: map[string]*openapi.Schema{
   320  					"foos": {
   321  						Type: "array",
   322  						Items: &openapi.Schema{
   323  							Type: "string",
   324  							Extension: map[string]interface{}{
   325  								"x-dcl-references": []interface{}{
   326  									map[interface{}]interface{}{
   327  										"resource": "Test1/Foo",
   328  										"field":    "name",
   329  									},
   330  								},
   331  							},
   332  						},
   333  					},
   334  				},
   335  			},
   336  			specSchema: &apiextensions.JSONSchemaProps{
   337  				Type: "object",
   338  				Properties: map[string]apiextensions.JSONSchemaProps{
   339  					"foos": {
   340  						Type: "array",
   341  						Items: &apiextensions.JSONSchemaPropsOrArray{
   342  							Schema: crdboilerplate.GetResourceReferenceSchemaBoilerplate(
   343  								"Allowed value: The Google Cloud resource name of a `Test1Foo` resource (format: `projects/{{project}}/foo/{{name}}`).",
   344  							),
   345  						},
   346  					},
   347  				},
   348  			},
   349  			statusSchema: nil,
   350  		},
   351  		{
   352  			name: "a list of multi-kinds reference",
   353  			dclSchema: &openapi.Schema{
   354  				Type: "object",
   355  				Properties: map[string]*openapi.Schema{
   356  					"foos": {
   357  						Type: "array",
   358  						Items: &openapi.Schema{
   359  							Type: "string",
   360  							Extension: map[string]interface{}{
   361  								"x-dcl-references": []interface{}{
   362  									map[interface{}]interface{}{
   363  										"resource": "Test1/Bar",
   364  										"field":    "selfLink",
   365  									},
   366  									map[interface{}]interface{}{
   367  										"resource": "Test2/Baz",
   368  										"field":    "selfLink",
   369  									},
   370  								},
   371  							},
   372  						},
   373  					},
   374  				},
   375  			},
   376  			specSchema: &apiextensions.JSONSchemaProps{
   377  				Type: "object",
   378  				Properties: map[string]apiextensions.JSONSchemaProps{
   379  					"foos": {
   380  						Type: "array",
   381  						Items: &apiextensions.JSONSchemaPropsOrArray{
   382  							Schema: crdboilerplate.GetMultiKindResourceReferenceSchemaBoilerplate(
   383  								"Allowed values:"+
   384  									"\n* The `selfLink` field of a `Test1Bar` resource."+
   385  									"\n* The `selfLink` field of a `Test2Baz` resource.",
   386  								[]string{"Test1Bar", "Test2Baz"},
   387  							),
   388  						},
   389  					},
   390  				},
   391  			},
   392  			statusSchema: nil,
   393  		},
   394  		{
   395  			name: "reference field for multiple kinds",
   396  			dclSchema: &openapi.Schema{
   397  				Type: "object",
   398  				Properties: map[string]*openapi.Schema{
   399  					"foo": {
   400  						Type: "string",
   401  						Extension: map[string]interface{}{
   402  							"x-dcl-references": []interface{}{
   403  								map[interface{}]interface{}{
   404  									"resource": "Test1/Bar",
   405  									"field":    "selfLink",
   406  								},
   407  								map[interface{}]interface{}{
   408  									"resource": "Test2/Baz",
   409  									"field":    "selfLink",
   410  								},
   411  							},
   412  						},
   413  					},
   414  				},
   415  				Required: []string{"foo"},
   416  			},
   417  			specSchema: &apiextensions.JSONSchemaProps{
   418  				Type: "object",
   419  				Properties: map[string]apiextensions.JSONSchemaProps{
   420  					"fooRef": *crdboilerplate.GetMultiKindResourceReferenceSchemaBoilerplate(
   421  						"Allowed values:"+
   422  							"\n* The `selfLink` field of a `Test1Bar` resource."+
   423  							"\n* The `selfLink` field of a `Test2Baz` resource.",
   424  						[]string{"Test1Bar", "Test2Baz"},
   425  					),
   426  				},
   427  				Required: []string{"fooRef"},
   428  			},
   429  			statusSchema: nil,
   430  		},
   431  		{
   432  			name: "reference to not-yet-supported resources",
   433  			dclSchema: &openapi.Schema{
   434  				Type: "object",
   435  				Properties: map[string]*openapi.Schema{
   436  					"foo": {
   437  						Type: "string",
   438  						Extension: map[string]interface{}{
   439  							"x-dcl-references": []interface{}{
   440  								map[interface{}]interface{}{
   441  									"resource": "Test1/NotYetSupportedKind",
   442  									"field":    "name",
   443  								},
   444  							},
   445  						},
   446  					},
   447  				},
   448  				Required: []string{"foo"},
   449  			},
   450  			specSchema: &apiextensions.JSONSchemaProps{
   451  				Type: "object",
   452  				Properties: map[string]apiextensions.JSONSchemaProps{
   453  					"fooRef": *markReferencedKindsNotSupported(
   454  						crdboilerplate.GetResourceReferenceSchemaBoilerplate(""),
   455  						[]string{"Test1NotYetSupportedKind"},
   456  					),
   457  				},
   458  				Required: []string{"fooRef"},
   459  			},
   460  		},
   461  		{
   462  			name: "the service of referenced resource is not declared",
   463  			dclSchema: &openapi.Schema{
   464  				Type: "object",
   465  				Properties: map[string]*openapi.Schema{
   466  					"foo": {
   467  						Type: "string",
   468  						Extension: map[string]interface{}{
   469  							"x-dcl-references": []interface{}{
   470  								map[interface{}]interface{}{
   471  									"resource": "SomeNotDeclaredService/Foo",
   472  									"field":    "name",
   473  								},
   474  							},
   475  						},
   476  					},
   477  				},
   478  				Required: []string{"foo"},
   479  			},
   480  			hasErrorOnSpecGeneration: true,
   481  		},
   482  		{
   483  			name: "referenced resource's (target) field is 'name' but its DCL schema (which contains 'x-dcl-id' extension) is not found",
   484  			dclSchema: &openapi.Schema{
   485  				Type: "object",
   486  				Properties: map[string]*openapi.Schema{
   487  					"foo": {
   488  						Type: "string",
   489  						Extension: map[string]interface{}{
   490  							"x-dcl-references": []interface{}{
   491  								map[interface{}]interface{}{
   492  									"resource": "Test1/FakeTFBasedResource",
   493  									"field":    "name",
   494  								},
   495  							},
   496  						},
   497  					},
   498  				},
   499  				Required: []string{"foo"},
   500  			},
   501  			hasErrorOnSpecGeneration: true,
   502  		},
   503  		{
   504  			name: "one hierarchical reference: projectRef",
   505  			dclSchema: &openapi.Schema{
   506  				Type: "object",
   507  				Properties: map[string]*openapi.Schema{
   508  					"project": {
   509  						Type: "string",
   510  						Extension: map[string]interface{}{
   511  							"x-dcl-references": []interface{}{
   512  								map[interface{}]interface{}{
   513  									"resource": "Cloudresourcemanager/Project",
   514  									"field":    "name",
   515  									"parent":   true,
   516  								},
   517  							},
   518  						},
   519  					},
   520  				},
   521  				Required: []string{"project"},
   522  			},
   523  			// TODO(b/186159460): Remove this field once all resources support
   524  			// hierarchical references.
   525  			resource: dclmetadata.Resource{
   526  				SupportsHierarchicalReferences: true,
   527  			},
   528  			specSchema: &apiextensions.JSONSchemaProps{
   529  				Type: "object",
   530  				Properties: map[string]apiextensions.JSONSchemaProps{
   531  					"projectRef": *resourceRefBoilerplateWithDescription(
   532  						"The Project that this resource belongs to.",
   533  						"Allowed value: The Google Cloud resource name of a `Project` resource (format: `projects/{{name}}`).",
   534  					),
   535  				},
   536  				Required: []string{"projectRef"},
   537  			},
   538  			statusSchema: nil,
   539  		},
   540  		{
   541  			name: "one hierarchical reference: folderRef",
   542  			dclSchema: &openapi.Schema{
   543  				Type: "object",
   544  				Properties: map[string]*openapi.Schema{
   545  					"folder": {
   546  						Type: "string",
   547  						Extension: map[string]interface{}{
   548  							"x-dcl-references": []interface{}{
   549  								map[interface{}]interface{}{
   550  									"resource": "Cloudresourcemanager/Folder",
   551  									"field":    "name",
   552  									"parent":   true,
   553  								},
   554  							},
   555  						},
   556  					},
   557  				},
   558  				Required: []string{"folder"},
   559  			},
   560  			// TODO(b/186159460): Remove this field once all resources support
   561  			// hierarchical references.
   562  			resource: dclmetadata.Resource{
   563  				SupportsHierarchicalReferences: true,
   564  			},
   565  			specSchema: &apiextensions.JSONSchemaProps{
   566  				Type: "object",
   567  				Properties: map[string]apiextensions.JSONSchemaProps{
   568  					"folderRef": *resourceRefBoilerplateWithDescription(
   569  						"The Folder that this resource belongs to.",
   570  						"Allowed value: The Google Cloud resource name of a `Folder` resource (format: `folders/{{name}}`).",
   571  					),
   572  				},
   573  				Required: []string{"folderRef"},
   574  			},
   575  			statusSchema: nil,
   576  		},
   577  		{
   578  			name: "one hierarchical reference: organizationRef",
   579  			dclSchema: &openapi.Schema{
   580  				Type: "object",
   581  				Properties: map[string]*openapi.Schema{
   582  					"organization": {
   583  						Type: "string",
   584  						Extension: map[string]interface{}{
   585  							"x-dcl-references": []interface{}{
   586  								map[interface{}]interface{}{
   587  									"resource": "Cloudresourcemanager/Organization",
   588  									"field":    "name",
   589  									"parent":   true,
   590  								},
   591  							},
   592  						},
   593  					},
   594  				},
   595  				Required: []string{"organization"},
   596  			},
   597  			// TODO(b/186159460): Remove this field once all resources support
   598  			// hierarchical references.
   599  			resource: dclmetadata.Resource{
   600  				SupportsHierarchicalReferences: true,
   601  			},
   602  			specSchema: &apiextensions.JSONSchemaProps{
   603  				Type: "object",
   604  				Properties: map[string]apiextensions.JSONSchemaProps{
   605  					"organizationRef": *markReferencedKindsNotSupported(
   606  						resourceRefBoilerplateWithDescription(
   607  							"The Organization that this resource belongs to.",
   608  							"Allowed value: The Google Cloud resource name of a Google Cloud Organization (format: `organizations/{{name}}`).",
   609  						),
   610  						[]string{"Organization"},
   611  					),
   612  				},
   613  				Required: []string{"organizationRef"},
   614  			},
   615  			statusSchema: nil,
   616  		},
   617  		{
   618  			name: "multiple hierarchical references: projectRef, folderRef, and organizationRef",
   619  			dclSchema: &openapi.Schema{
   620  				Type: "object",
   621  				Properties: map[string]*openapi.Schema{
   622  					"parent": {
   623  						Type: "string",
   624  						Extension: map[string]interface{}{
   625  							"x-dcl-references": []interface{}{
   626  								map[interface{}]interface{}{
   627  									"resource": "Cloudresourcemanager/Project",
   628  									"field":    "name",
   629  									"parent":   true,
   630  								},
   631  								map[interface{}]interface{}{
   632  									"resource": "Cloudresourcemanager/Folder",
   633  									"field":    "name",
   634  									"parent":   true,
   635  								},
   636  								map[interface{}]interface{}{
   637  									"resource": "Cloudresourcemanager/Organization",
   638  									"field":    "name",
   639  									"parent":   true,
   640  								},
   641  							},
   642  						},
   643  					},
   644  				},
   645  				Required: []string{"parent"},
   646  			},
   647  			// TODO(b/186159460): Remove this field once all resources support
   648  			// hierarchical references.
   649  			resource: dclmetadata.Resource{
   650  				SupportsHierarchicalReferences: true,
   651  			},
   652  			specSchema: &apiextensions.JSONSchemaProps{
   653  				Type: "object",
   654  				Properties: map[string]apiextensions.JSONSchemaProps{
   655  					"projectRef": *resourceRefBoilerplateWithDescription(
   656  						"The Project that this resource belongs to. Only one of [projectRef, folderRef, organizationRef] may be specified.",
   657  						"Allowed value: The Google Cloud resource name of a `Project` resource (format: `projects/{{name}}`).",
   658  					),
   659  					"folderRef": *resourceRefBoilerplateWithDescription(
   660  						"The Folder that this resource belongs to. Only one of [projectRef, folderRef, organizationRef] may be specified.",
   661  						"Allowed value: The Google Cloud resource name of a `Folder` resource (format: `folders/{{name}}`).",
   662  					),
   663  					"organizationRef": *markReferencedKindsNotSupported(
   664  						resourceRefBoilerplateWithDescription(
   665  							"The Organization that this resource belongs to. Only one of [projectRef, folderRef, organizationRef] may be specified.",
   666  							"Allowed value: The Google Cloud resource name of a Google Cloud Organization (format: `organizations/{{name}}`).",
   667  						),
   668  						[]string{"Organization"},
   669  					),
   670  				},
   671  				OneOf: []apiextensions.JSONSchemaProps{
   672  					{Required: []string{"projectRef"}},
   673  					{Required: []string{"folderRef"}},
   674  					{Required: []string{"organizationRef"}},
   675  				},
   676  			},
   677  			statusSchema: nil,
   678  		},
   679  		{
   680  			name: "multiple hierarchical references: folderRef and organizationRef",
   681  			dclSchema: &openapi.Schema{
   682  				Type: "object",
   683  				Properties: map[string]*openapi.Schema{
   684  					"parent": {
   685  						Type: "string",
   686  						Extension: map[string]interface{}{
   687  							"x-dcl-references": []interface{}{
   688  								map[interface{}]interface{}{
   689  									"resource": "Cloudresourcemanager/Folder",
   690  									"field":    "name",
   691  									"parent":   true,
   692  								},
   693  								map[interface{}]interface{}{
   694  									"resource": "Cloudresourcemanager/Organization",
   695  									"field":    "name",
   696  									"parent":   true,
   697  								},
   698  							},
   699  						},
   700  					},
   701  				},
   702  				Required: []string{"parent"},
   703  			},
   704  			// TODO(b/186159460): Remove this field once all resources support
   705  			// hierarchical references.
   706  			resource: dclmetadata.Resource{
   707  				SupportsHierarchicalReferences: true,
   708  			},
   709  			specSchema: &apiextensions.JSONSchemaProps{
   710  				Type: "object",
   711  				Properties: map[string]apiextensions.JSONSchemaProps{
   712  					"folderRef": *resourceRefBoilerplateWithDescription(
   713  						"The Folder that this resource belongs to. Only one of [folderRef, organizationRef] may be specified.",
   714  						"Allowed value: The Google Cloud resource name of a `Folder` resource (format: `folders/{{name}}`).",
   715  					),
   716  					"organizationRef": *markReferencedKindsNotSupported(
   717  						resourceRefBoilerplateWithDescription(
   718  							"The Organization that this resource belongs to. Only one of [folderRef, organizationRef] may be specified.",
   719  							"Allowed value: The Google Cloud resource name of a Google Cloud Organization (format: `organizations/{{name}}`).",
   720  						),
   721  						[]string{"Organization"},
   722  					),
   723  				},
   724  				OneOf: []apiextensions.JSONSchemaProps{
   725  					{Required: []string{"folderRef"}},
   726  					{Required: []string{"organizationRef"}},
   727  				},
   728  			},
   729  			statusSchema: nil,
   730  		},
   731  		{
   732  			name: "resource that supports container annotations and has one hierarchical reference: projectRef",
   733  			dclSchema: &openapi.Schema{
   734  				Type: "object",
   735  				Properties: map[string]*openapi.Schema{
   736  					"project": {
   737  						Type: "string",
   738  						Extension: map[string]interface{}{
   739  							"x-dcl-references": []interface{}{
   740  								map[interface{}]interface{}{
   741  									"resource": "Cloudresourcemanager/Project",
   742  									"field":    "name",
   743  									"parent":   true,
   744  								},
   745  							},
   746  						},
   747  					},
   748  				},
   749  				Required: []string{"project"},
   750  			},
   751  			resource: dclmetadata.Resource{
   752  				SupportsContainerAnnotations: true,
   753  				// TODO(b/186159460): Remove this field once all resources
   754  				// support hierarchical references.
   755  				SupportsHierarchicalReferences: true,
   756  			},
   757  			specSchema: &apiextensions.JSONSchemaProps{
   758  				Type: "object",
   759  				Properties: map[string]apiextensions.JSONSchemaProps{
   760  					"projectRef": *resourceRefBoilerplateWithDescription(
   761  						"The Project that this resource belongs to.",
   762  						"Allowed value: The Google Cloud resource name of a `Project` resource (format: `projects/{{name}}`).",
   763  					),
   764  				},
   765  			},
   766  			statusSchema: nil,
   767  		},
   768  		{
   769  			name: "resource that supports container annotations and has multiple hierarchical references: projectRef, folderRef, and organizationRef",
   770  			dclSchema: &openapi.Schema{
   771  				Type: "object",
   772  				Properties: map[string]*openapi.Schema{
   773  					"parent": {
   774  						Type: "string",
   775  						Extension: map[string]interface{}{
   776  							"x-dcl-references": []interface{}{
   777  								map[interface{}]interface{}{
   778  									"resource": "Cloudresourcemanager/Project",
   779  									"field":    "name",
   780  									"parent":   true,
   781  								},
   782  								map[interface{}]interface{}{
   783  									"resource": "Cloudresourcemanager/Folder",
   784  									"field":    "name",
   785  									"parent":   true,
   786  								},
   787  								map[interface{}]interface{}{
   788  									"resource": "Cloudresourcemanager/Organization",
   789  									"field":    "name",
   790  									"parent":   true,
   791  								},
   792  							},
   793  						},
   794  					},
   795  				},
   796  				Required: []string{"parent"},
   797  			},
   798  			resource: dclmetadata.Resource{
   799  				SupportsContainerAnnotations: true,
   800  				// TODO(b/186159460): Remove this field once all resources
   801  				// support hierarchical references.
   802  				SupportsHierarchicalReferences: true,
   803  			},
   804  			specSchema: &apiextensions.JSONSchemaProps{
   805  				Type: "object",
   806  				Properties: map[string]apiextensions.JSONSchemaProps{
   807  					"projectRef": *resourceRefBoilerplateWithDescription(
   808  						"The Project that this resource belongs to. Only one of [projectRef, folderRef, organizationRef] may be specified.",
   809  						"Allowed value: The Google Cloud resource name of a `Project` resource (format: `projects/{{name}}`).",
   810  					),
   811  					"folderRef": *resourceRefBoilerplateWithDescription(
   812  						"The Folder that this resource belongs to. Only one of [projectRef, folderRef, organizationRef] may be specified.",
   813  						"Allowed value: The Google Cloud resource name of a `Folder` resource (format: `folders/{{name}}`).",
   814  					),
   815  					"organizationRef": *markReferencedKindsNotSupported(
   816  						resourceRefBoilerplateWithDescription(
   817  							"The Organization that this resource belongs to. Only one of [projectRef, folderRef, organizationRef] may be specified.",
   818  							"Allowed value: The Google Cloud resource name of a Google Cloud Organization (format: `organizations/{{name}}`).",
   819  						),
   820  						[]string{"Organization"},
   821  					),
   822  				},
   823  				OneOf: []apiextensions.JSONSchemaProps{
   824  					{Required: []string{"projectRef"}},
   825  					{Required: []string{"folderRef"}},
   826  					{Required: []string{"organizationRef"}},
   827  					{
   828  						Not: &apiextensions.JSONSchemaProps{
   829  							AnyOf: []apiextensions.JSONSchemaProps{
   830  								{Required: []string{"projectRef"}},
   831  								{Required: []string{"folderRef"}},
   832  								{Required: []string{"organizationRef"}},
   833  							},
   834  						},
   835  					},
   836  				},
   837  			},
   838  			statusSchema: nil,
   839  		},
   840  		{
   841  			name: "container field will be ignored",
   842  			dclSchema: &openapi.Schema{
   843  				Type: "object",
   844  				Properties: map[string]*openapi.Schema{
   845  					"foo": {
   846  						Type: "string",
   847  					},
   848  					"project": {
   849  						Type: "string",
   850  					},
   851  				},
   852  				Required: []string{"foo", "project"},
   853  			},
   854  			specSchema: &apiextensions.JSONSchemaProps{
   855  				Type: "object",
   856  				Properties: map[string]apiextensions.JSONSchemaProps{
   857  					"foo": {
   858  						Type: "string",
   859  					},
   860  				},
   861  				Required: []string{"foo"},
   862  			},
   863  			statusSchema: nil,
   864  		},
   865  		{
   866  			name: "name field will be converted to ResourceID",
   867  			dclSchema: &openapi.Schema{
   868  				Type: "object",
   869  				Properties: map[string]*openapi.Schema{
   870  					"foo": {
   871  						Type: "string",
   872  					},
   873  					"name": {
   874  						Type: "string",
   875  					},
   876  				},
   877  				Required: []string{"foo", "name"},
   878  			},
   879  			specSchema: &apiextensions.JSONSchemaProps{
   880  				Type: "object",
   881  				Properties: map[string]apiextensions.JSONSchemaProps{
   882  					"foo": {
   883  						Type: "string",
   884  					},
   885  					"resourceID": {
   886  						Description: GenerateResourceIDFieldDescription("name", false),
   887  						Type:        "string",
   888  					},
   889  				},
   890  				Required: []string{"foo"},
   891  			},
   892  			statusSchema: nil,
   893  		},
   894  		{
   895  			name: "string-object maps",
   896  			dclSchema: &openapi.Schema{
   897  				Type: "object",
   898  				Properties: map[string]*openapi.Schema{
   899  					"baz": {
   900  						Type: "object",
   901  						AdditionalProperties: &openapi.Schema{
   902  							Type: "object",
   903  							Properties: map[string]*openapi.Schema{
   904  								"objectField1": {
   905  									Type: "string",
   906  								},
   907  								"objectField2": {
   908  									ReadOnly: true,
   909  									Type:     "integer",
   910  								},
   911  								"objectField3": {
   912  									Type: "array",
   913  									Items: &openapi.Schema{
   914  										Type: "string",
   915  									},
   916  								},
   917  								"foo": {
   918  									Type: "string",
   919  									Extension: map[string]interface{}{
   920  										"x-dcl-references": []interface{}{
   921  											map[interface{}]interface{}{
   922  												"resource": "Test1/Foo",
   923  												"field":    "name",
   924  											},
   925  										},
   926  									},
   927  								},
   928  							},
   929  						},
   930  					},
   931  				},
   932  			},
   933  			specSchema: &apiextensions.JSONSchemaProps{
   934  				Type: "object",
   935  				Properties: map[string]apiextensions.JSONSchemaProps{
   936  					"baz": {
   937  						Type: "object",
   938  						AdditionalProperties: &apiextensions.JSONSchemaPropsOrBool{
   939  							Schema: &apiextensions.JSONSchemaProps{
   940  								Type: "object",
   941  								Properties: map[string]apiextensions.JSONSchemaProps{
   942  									"objectField1": {
   943  										Type: "string",
   944  									},
   945  									"objectField2": {
   946  										Type: "integer",
   947  									},
   948  									"objectField3": {
   949  										Type: "array",
   950  										Items: &apiextensions.JSONSchemaPropsOrArray{
   951  											Schema: &apiextensions.JSONSchemaProps{
   952  												Type: "string",
   953  											},
   954  										},
   955  									},
   956  									"fooRef": *crdboilerplate.GetResourceReferenceSchemaBoilerplate(
   957  										"Allowed value: The Google Cloud resource name of a `Test1Foo` resource (format: `projects/{{project}}/foo/{{name}}`).",
   958  									),
   959  								},
   960  							},
   961  						},
   962  					},
   963  				},
   964  			},
   965  			statusSchema: nil,
   966  		},
   967  		{
   968  			name: "array type, additionalProperties",
   969  			dclSchema: &openapi.Schema{
   970  				Type: "object",
   971  				Properties: map[string]*openapi.Schema{
   972  					"foo": {
   973  						Type: "array",
   974  						Items: &openapi.Schema{
   975  							Type: "string",
   976  						},
   977  					},
   978  					"baz": {
   979  						Type: "object",
   980  						AdditionalProperties: &openapi.Schema{
   981  							Type: "string",
   982  						},
   983  					},
   984  					"qux": {
   985  						Type: "array",
   986  						Items: &openapi.Schema{
   987  							Type: "string",
   988  						},
   989  						ReadOnly: true,
   990  					},
   991  				},
   992  				Required: []string{"foo"},
   993  			},
   994  			specSchema: &apiextensions.JSONSchemaProps{
   995  				Type: "object",
   996  				Properties: map[string]apiextensions.JSONSchemaProps{
   997  					"foo": {
   998  						Type: "array",
   999  						Items: &apiextensions.JSONSchemaPropsOrArray{
  1000  							Schema: &apiextensions.JSONSchemaProps{
  1001  								Type: "string",
  1002  							},
  1003  						},
  1004  					},
  1005  					"baz": {
  1006  						Type: "object",
  1007  						AdditionalProperties: &apiextensions.JSONSchemaPropsOrBool{
  1008  							Schema: &apiextensions.JSONSchemaProps{
  1009  								Type: "string",
  1010  							},
  1011  						},
  1012  					},
  1013  				},
  1014  				Required: []string{"foo"},
  1015  			},
  1016  			statusSchema: &apiextensions.JSONSchemaProps{
  1017  				Type: "object",
  1018  				Properties: map[string]apiextensions.JSONSchemaProps{
  1019  					"qux": {
  1020  						Type: "array",
  1021  						Items: &apiextensions.JSONSchemaPropsOrArray{
  1022  							Schema: &apiextensions.JSONSchemaProps{
  1023  								Type: "string",
  1024  							},
  1025  						},
  1026  					},
  1027  				},
  1028  			},
  1029  		},
  1030  		{
  1031  			name: "Enum is not exposed by design in CRD schema",
  1032  			dclSchema: &openapi.Schema{
  1033  				Type: "object",
  1034  				Properties: map[string]*openapi.Schema{
  1035  					"foo": {
  1036  						Type: "string",
  1037  						Enum: []string{"VAL1", "VAL2"},
  1038  					},
  1039  					"baz": {
  1040  						Type:     "string",
  1041  						ReadOnly: true,
  1042  						Enum:     []string{"VAL1", "VAL2"},
  1043  					},
  1044  				},
  1045  				Required: []string{"foo"},
  1046  			},
  1047  			specSchema: &apiextensions.JSONSchemaProps{
  1048  				Type: "object",
  1049  				Properties: map[string]apiextensions.JSONSchemaProps{
  1050  					"foo": {
  1051  						Type: "string",
  1052  					},
  1053  				},
  1054  				Required: []string{"foo"},
  1055  			},
  1056  			statusSchema: &apiextensions.JSONSchemaProps{
  1057  				Type: "object",
  1058  				Properties: map[string]apiextensions.JSONSchemaProps{
  1059  					"baz": {
  1060  						Type: "string",
  1061  					},
  1062  				},
  1063  			},
  1064  		},
  1065  		{
  1066  			name: "read-only fields in arrays will be preserved in spec",
  1067  			dclSchema: &openapi.Schema{
  1068  				Type: "object",
  1069  				Properties: map[string]*openapi.Schema{
  1070  					"foo": {
  1071  						Type: "array",
  1072  						Items: &openapi.Schema{
  1073  							Type: "object",
  1074  							Properties: map[string]*openapi.Schema{
  1075  								"nestedField1": {
  1076  									Type: "boolean",
  1077  								},
  1078  								"nestedField2": {
  1079  									Type:     "string",
  1080  									ReadOnly: true,
  1081  								},
  1082  								"nestedObject": {
  1083  									Type: "object",
  1084  									Properties: map[string]*openapi.Schema{
  1085  										"state": {
  1086  											Type:     "string",
  1087  											ReadOnly: true,
  1088  										},
  1089  										"name": {
  1090  											Type: "string",
  1091  										},
  1092  									},
  1093  								},
  1094  								"readOnlySensitiveField": {
  1095  									Type:     "string",
  1096  									ReadOnly: true,
  1097  									Extension: map[string]interface{}{
  1098  										"x-dcl-sensitive": true,
  1099  									},
  1100  								},
  1101  								"readOnlyReferenceField": {
  1102  									Type:     "string",
  1103  									ReadOnly: true,
  1104  									Extension: map[string]interface{}{
  1105  										"x-dcl-references": []interface{}{
  1106  											map[interface{}]interface{}{
  1107  												"resource": "FakeService/FakeKind",
  1108  												"field":    "name",
  1109  											},
  1110  										},
  1111  									},
  1112  								},
  1113  							},
  1114  							Required: []string{"nestedField1"},
  1115  						},
  1116  					},
  1117  					"bar": {
  1118  						Type: "integer",
  1119  					},
  1120  				},
  1121  				Required: []string{"foo"},
  1122  			},
  1123  			specSchema: &apiextensions.JSONSchemaProps{
  1124  				Type: "object",
  1125  				Properties: map[string]apiextensions.JSONSchemaProps{
  1126  					"foo": {
  1127  						Type: "array",
  1128  						Items: &apiextensions.JSONSchemaPropsOrArray{
  1129  							Schema: &apiextensions.JSONSchemaProps{
  1130  								Type: "object",
  1131  								Properties: map[string]apiextensions.JSONSchemaProps{
  1132  									"nestedField1": {
  1133  										Type: "boolean",
  1134  									},
  1135  									"nestedField2": {
  1136  										Type: "string",
  1137  									},
  1138  									"nestedObject": {
  1139  										Type: "object",
  1140  										Properties: map[string]apiextensions.JSONSchemaProps{
  1141  											"name": {
  1142  												Type: "string",
  1143  											},
  1144  											"state": {
  1145  												Type: "string",
  1146  											},
  1147  										},
  1148  									},
  1149  									"readOnlySensitiveField": {
  1150  										Type: "string",
  1151  									},
  1152  									"readOnlyReferenceField": {
  1153  										Type: "string",
  1154  									},
  1155  								},
  1156  								Required: []string{"nestedField1"},
  1157  							},
  1158  						},
  1159  					},
  1160  					"bar": {
  1161  						Type: "integer",
  1162  					},
  1163  				},
  1164  				Required: []string{"foo"},
  1165  			},
  1166  			statusSchema: nil,
  1167  		},
  1168  		{
  1169  			name: "empty spec",
  1170  			dclSchema: &openapi.Schema{
  1171  				Type: "object",
  1172  			},
  1173  			specSchema: &apiextensions.JSONSchemaProps{
  1174  				Type: "object",
  1175  			},
  1176  			statusSchema: nil,
  1177  		},
  1178  		{
  1179  			// in this case, the labels field should be removed from the
  1180  			// spec, as the values will be sourced from Kubernetes labels.
  1181  			name: "field specified as x-dcl-labels",
  1182  			dclSchema: &openapi.Schema{
  1183  				Type: "object",
  1184  				Extension: map[string]interface{}{
  1185  					"x-dcl-labels": "labels",
  1186  				},
  1187  				Properties: map[string]*openapi.Schema{
  1188  					"labels": {
  1189  						Type: "object",
  1190  						AdditionalProperties: &openapi.Schema{
  1191  							Type: "string",
  1192  						},
  1193  					},
  1194  				},
  1195  			},
  1196  			specSchema: &apiextensions.JSONSchemaProps{
  1197  				Type: "object",
  1198  			},
  1199  			statusSchema: nil,
  1200  		},
  1201  		{
  1202  			// labels field should not be removed from the spec. This was
  1203  			// the previous no longer desired behavior, so verifying there is
  1204  			// no regression.
  1205  			name: "labels field exists, but not specified as x-dcl-labels",
  1206  			dclSchema: &openapi.Schema{
  1207  				Type: "object",
  1208  				Properties: map[string]*openapi.Schema{
  1209  					"labels": {
  1210  						Type: "object",
  1211  						AdditionalProperties: &openapi.Schema{
  1212  							Type: "string",
  1213  						},
  1214  					},
  1215  				},
  1216  			},
  1217  			specSchema: &apiextensions.JSONSchemaProps{
  1218  				Type: "object",
  1219  				Properties: map[string]apiextensions.JSONSchemaProps{
  1220  					"labels": {
  1221  						Type: "object",
  1222  						AdditionalProperties: &apiextensions.JSONSchemaPropsOrBool{
  1223  							Schema: &apiextensions.JSONSchemaProps{
  1224  								Type: "string",
  1225  							},
  1226  						},
  1227  					},
  1228  				},
  1229  			},
  1230  			statusSchema: nil,
  1231  		},
  1232  	}
  1233  
  1234  	smLoader := servicemappingloader.NewFromServiceMappings(test.FakeServiceMappingsWithHierarchicalResources())
  1235  	serviceMetadataLoader := dclmetadata.NewFromServiceList(testservicemetadataloader.FakeServiceMetadataWithHierarchicalResources())
  1236  	dclSchemaLoader := testdclschemaloader.New(dclSchemaMap())
  1237  	allSupportedGVKs := supportedgvks.All(smLoader, serviceMetadataLoader)
  1238  	a := New(serviceMetadataLoader, dclSchemaLoader, allSupportedGVKs)
  1239  	for _, tc := range tests {
  1240  		tc := tc
  1241  		t.Run(tc.name, func(t *testing.T) {
  1242  			t.Parallel()
  1243  			spec, err := a.generateSpecJSONSchema(tc.dclSchema, tc.resource)
  1244  			if tc.hasErrorOnSpecGeneration {
  1245  				if err == nil {
  1246  					t.Fatalf("got nil, but expect to get error on generating the spec")
  1247  				}
  1248  				return
  1249  			}
  1250  			if err != nil {
  1251  				t.Fatalf("error generating spec schema: %v", err)
  1252  			}
  1253  			if !test.Equals(t, tc.specSchema, spec) {
  1254  				t.Fatalf("unexpected spec diff (-want +got): \n%v", cmp.Diff(tc.specSchema, spec))
  1255  			}
  1256  			status, err := generateStatusJSONSchema(tc.dclSchema)
  1257  			if err != nil {
  1258  				t.Fatalf("error generating status schema: %v", err)
  1259  			}
  1260  			if !test.Equals(t, tc.statusSchema, status) {
  1261  				t.Fatalf("unexpected status diff (-want +got): \n%v", cmp.Diff(tc.statusSchema, status))
  1262  			}
  1263  		})
  1264  	}
  1265  }
  1266  
  1267  func dclSchemaMap() map[string]*openapi.Schema {
  1268  	return map[string]*openapi.Schema{
  1269  		"test1_beta_foo": &openapi.Schema{
  1270  			Extension: map[string]interface{}{
  1271  				"x-dcl-id": "projects/{{project}}/foo/{{name}}",
  1272  			},
  1273  		},
  1274  		"test1_beta_bar": &openapi.Schema{
  1275  			Extension: map[string]interface{}{
  1276  				"x-dcl-id": "projects/{{project}}/bar/{{name}}",
  1277  			},
  1278  		},
  1279  
  1280  		// Add the following to the list of fake DCL schemas to allow for our
  1281  		// test to test resources that reference hierarchical resources
  1282  		// (e.g. "Cloudresourcemanager/Project").
  1283  		"cloudresourcemanager_ga_project": &openapi.Schema{
  1284  			Extension: map[string]interface{}{
  1285  				"x-dcl-id": "projects/{{name}}",
  1286  			},
  1287  		},
  1288  		"cloudresourcemanager_ga_folder": &openapi.Schema{
  1289  			Extension: map[string]interface{}{
  1290  				"x-dcl-id": "folders/{{name}}",
  1291  			},
  1292  		},
  1293  	}
  1294  }
  1295  
  1296  func resourceRefBoilerplateWithDescription(description, externalRefDescription string) *apiextensions.JSONSchemaProps {
  1297  	schema := crdboilerplate.GetResourceReferenceSchemaBoilerplate(externalRefDescription)
  1298  	schema.Description = description
  1299  	return schema
  1300  }
  1301  
  1302  func markReferencedKindsNotSupported(schema *apiextensions.JSONSchemaProps, kinds []string) *apiextensions.JSONSchemaProps {
  1303  	s := schema.DeepCopy()
  1304  	MarkReferencedKindsNotSupported(s, kinds)
  1305  	return s
  1306  }
  1307  

View as plain text