diff --git a/.golangci.yml b/.golangci.yml index cf5891f9c..1576fd4b2 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -45,6 +45,7 @@ linters: - duplicatemarkers - integers - jsontags + - kustomizeref - maxlength # NOTE: we have a number of boolean fields. Should we convert them to # string? diff --git a/Makefile b/Makefile index 7537a25da..c6388d49a 100644 --- a/Makefile +++ b/Makefile @@ -58,7 +58,7 @@ modules: go mod tidy .PHONY: generate -generate: generate-resources generate-controller-gen generate-codegen generate-go generate-docs modules manifests +generate: generate-resources generate-controller-gen generate-kustomizeconfig generate-codegen generate-go generate-docs modules manifests .PHONY: generate-resources generate-resources: @@ -68,6 +68,10 @@ generate-resources: generate-controller-gen: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." +.PHONY: generate-kustomizeconfig +generate-kustomizeconfig: generate-resources generate-controller-gen ## Generate examples/components/kustomizeconfig/kustomizeconfig.yaml from +orc:kustomize:ref markers on the API types. + go run ./cmd/kustomizeconfig-generator + .PHONY: generate-codegen generate-codegen: generate-controller-gen ## codegen requires DeepCopy etc ./hack/update-codegen.sh diff --git a/api/v1alpha1/addressscope_types.go b/api/v1alpha1/addressscope_types.go index 8a28e4585..7bfddc14b 100644 --- a/api/v1alpha1/addressscope_types.go +++ b/api/v1alpha1/addressscope_types.go @@ -26,6 +26,7 @@ type AddressScopeResourceSpec struct { // projectRef is a reference to the ORC Project which this resource is associated with. // +optional // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="projectRef is immutable" + // +orc:kustomize:ref=Project ProjectRef *KubernetesNameRef `json:"projectRef,omitempty"` // ipVersion is the IP protocol version. @@ -51,6 +52,7 @@ type AddressScopeFilter struct { // projectRef is a reference to the ORC Project which this resource is associated with. // +optional + // +orc:kustomize:ref=Project ProjectRef *KubernetesNameRef `json:"projectRef,omitempty"` // ipVersion is the IP protocol version. diff --git a/api/v1alpha1/applicationcredential_types.go b/api/v1alpha1/applicationcredential_types.go index bcb11a3dd..457b3ac24 100644 --- a/api/v1alpha1/applicationcredential_types.go +++ b/api/v1alpha1/applicationcredential_types.go @@ -47,6 +47,7 @@ type ApplicationCredentialAccessRule struct { // serviceRef identifier for the service that the application credential is permitted to access // +optional + // +orc:kustomize:ref=Service ServiceRef *KubernetesNameRef `json:"serviceRef,omitempty"` } @@ -67,6 +68,7 @@ type ApplicationCredentialResourceSpec struct { // userRef is a reference to the ORC User which this resource is associated with. // Note: Due to the nature of the OpenStack API, managing application credentials for a user different than the one ORC is authenticated against can be computationally expensive. In the worst case, all application credentials of all users have to be queried. // +required + // +orc:kustomize:ref=User UserRef KubernetesNameRef `json:"userRef,omitempty"` // unrestricted is a flag indicating whether the application credential may be used for creation or destruction of other application credentials or trusts @@ -75,12 +77,14 @@ type ApplicationCredentialResourceSpec struct { // secretRef is a reference to a Secret containing the application credential secret // +required + // +orc:kustomize:ref=Secret SecretRef KubernetesNameRef `json:"secretRef,omitempty"` // roleRefs may only contain roles that the user has assigned on the project. If not provided, the roles assigned to the application credential will be the same as the roles in the current token. // +kubebuilder:validation:MaxItems:=256 // +listType=atomic // +optional + // +orc:kustomize:ref=Role RoleRefs []KubernetesNameRef `json:"roleRefs,omitempty"` // accessRules is a list of fine grained access control rules @@ -100,6 +104,7 @@ type ApplicationCredentialFilter struct { // userRef is a reference to the ORC User which this resource is associated with. // Note: Due to the nature of the OpenStack API, managing application credentials for a user different than the one ORC is authenticated against can be computationally expensive. In the worst case, all application credentials of all users have to be queried. // +required + // +orc:kustomize:ref=User UserRef KubernetesNameRef `json:"userRef,omitempty"` // name of the existing resource diff --git a/api/v1alpha1/credentials_ref.go b/api/v1alpha1/credentials_ref.go index 6d15e5d30..fc42481d1 100644 --- a/api/v1alpha1/credentials_ref.go +++ b/api/v1alpha1/credentials_ref.go @@ -31,6 +31,7 @@ type CloudCredentialsReference struct { // +required // +kubebuilder:validation:MinLength:=1 // +kubebuilder:validation:MaxLength:=253 + // +orc:kustomize:ref=Secret SecretName string `json:"secretName,omitempty"` // cloudName specifies the name of the entry in the clouds.yaml file to use. diff --git a/api/v1alpha1/endpoint_types.go b/api/v1alpha1/endpoint_types.go index fc2e9c5cc..a5629e2d1 100644 --- a/api/v1alpha1/endpoint_types.go +++ b/api/v1alpha1/endpoint_types.go @@ -42,6 +42,7 @@ type EndpointResourceSpec struct { // serviceRef is a reference to the ORC Service which this resource is associated with. // +required // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="serviceRef is immutable" + // +orc:kustomize:ref=Service ServiceRef KubernetesNameRef `json:"serviceRef,omitempty"` } @@ -55,6 +56,7 @@ type EndpointFilter struct { // serviceRef is a reference to the ORC Service which this resource is associated with. // +optional + // +orc:kustomize:ref=Service ServiceRef *KubernetesNameRef `json:"serviceRef,omitempty"` // url is the URL of the existing endpoint. diff --git a/api/v1alpha1/floatingip_types.go b/api/v1alpha1/floatingip_types.go index fd2739aba..12ce9be18 100644 --- a/api/v1alpha1/floatingip_types.go +++ b/api/v1alpha1/floatingip_types.go @@ -29,15 +29,18 @@ type FloatingIPFilter struct { // floatingNetworkRef is a reference to the ORC Network which this resource is associated with. // +optional + // +orc:kustomize:ref=Network FloatingNetworkRef *KubernetesNameRef `json:"floatingNetworkRef,omitempty"` // portRef is a reference to the ORC Port which this resource is associated with. // +optional + // +orc:kustomize:ref=Port PortRef *KubernetesNameRef `json:"portRef,omitempty"` // projectRef is a reference to the ORC Project this resource is associated with. // Typically, only used by admin. // +optional + // +orc:kustomize:ref=Project ProjectRef *KubernetesNameRef `json:"projectRef,omitempty"` // status is the status of the floatingip. @@ -64,11 +67,13 @@ type FloatingIPResourceSpec struct { // floatingNetworkRef references the network to which the floatingip is associated. // +optional // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="floatingNetworkRef is immutable" + // +orc:kustomize:ref=Network FloatingNetworkRef *KubernetesNameRef `json:"floatingNetworkRef,omitempty"` // floatingSubnetRef references the subnet to which the floatingip is associated. // +optional // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="floatingSubnetRef is immutable" + // +orc:kustomize:ref=Subnet FloatingSubnetRef *KubernetesNameRef `json:"floatingSubnetRef,omitempty"` // floatingIP is the IP that will be assigned to the floatingip. If not set, it will @@ -80,6 +85,7 @@ type FloatingIPResourceSpec struct { // portRef is a reference to the ORC Port which this resource is associated with. // +optional // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="portRef is immutable" + // +orc:kustomize:ref=Port PortRef *KubernetesNameRef `json:"portRef,omitempty"` // fixedIP is the IP address of the port to which the floatingip is associated. @@ -91,6 +97,7 @@ type FloatingIPResourceSpec struct { // Typically, only used by admin. // +optional // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="projectRef is immutable" + // +orc:kustomize:ref=Project ProjectRef *KubernetesNameRef `json:"projectRef,omitempty"` } diff --git a/api/v1alpha1/group_types.go b/api/v1alpha1/group_types.go index f3719f5ae..d8849f9b8 100644 --- a/api/v1alpha1/group_types.go +++ b/api/v1alpha1/group_types.go @@ -32,6 +32,7 @@ type GroupResourceSpec struct { // domainRef is a reference to the ORC Domain which this resource is associated with. // +optional // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="domainRef is immutable" + // +orc:kustomize:ref=Domain DomainRef *KubernetesNameRef `json:"domainRef,omitempty"` } @@ -44,6 +45,7 @@ type GroupFilter struct { // domainRef is a reference to the ORC Domain which this resource is associated with. // +optional + // +orc:kustomize:ref=Domain DomainRef *KubernetesNameRef `json:"domainRef,omitempty"` } diff --git a/api/v1alpha1/network_types.go b/api/v1alpha1/network_types.go index 18abf1e4e..2bbca597b 100644 --- a/api/v1alpha1/network_types.go +++ b/api/v1alpha1/network_types.go @@ -114,6 +114,7 @@ type NetworkResourceSpec struct { // Typically, only used by admin. // +optional // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="projectRef is immutable" + // +orc:kustomize:ref=Project ProjectRef *KubernetesNameRef `json:"projectRef,omitempty"` } @@ -136,6 +137,7 @@ type NetworkFilter struct { // projectRef is a reference to the ORC Project this resource is associated with. // Typically, only used by admin. // +optional + // +orc:kustomize:ref=Project ProjectRef *KubernetesNameRef `json:"projectRef,omitempty"` FilterByNeutronTags `json:",inline"` diff --git a/api/v1alpha1/port_types.go b/api/v1alpha1/port_types.go index 108dc8c5f..babc62569 100644 --- a/api/v1alpha1/port_types.go +++ b/api/v1alpha1/port_types.go @@ -29,11 +29,13 @@ type PortFilter struct { // networkRef is a reference to the ORC Network which this port is associated with. // +optional + // +orc:kustomize:ref=Network NetworkRef KubernetesNameRef `json:"networkRef"` // projectRef is a reference to the ORC Project this resource is associated with. // Typically, only used by admin. // +optional + // +orc:kustomize:ref=Project ProjectRef *KubernetesNameRef `json:"projectRef,omitempty"` // adminStateUp is the administrative state of the port, @@ -66,6 +68,7 @@ type HostID struct { // the Server's status.resource.hostID field. // This is mutually exclusive with id. // +optional + // +orc:kustomize:ref=Server ServerRef KubernetesNameRef `json:"serverRef,omitempty"` } @@ -106,6 +109,7 @@ type Address struct { // subnetRef references the subnet from which to allocate the IP // address. // +required + // +orc:kustomize:ref=Subnet SubnetRef KubernetesNameRef `json:"subnetRef,omitempty"` } @@ -148,6 +152,7 @@ type PortResourceSpec struct { // networkRef is a reference to the ORC Network which this port is associated with. // +required // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="networkRef is immutable" + // +orc:kustomize:ref=Network NetworkRef KubernetesNameRef `json:"networkRef,omitempty"` // tags is a list of tags which will be applied to the port. @@ -180,6 +185,7 @@ type PortResourceSpec struct { // +kubebuilder:validation:MaxItems:=64 // +listType=set // +optional + // +orc:kustomize:ref=SecurityGroup SecurityGroupRefs []KubernetesNameRef `json:"securityGroupRefs,omitempty"` // vnicType specifies the type of vNIC which this port should be @@ -207,6 +213,7 @@ type PortResourceSpec struct { // Typically, only used by admin. // +optional // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="projectRef is immutable" + // +orc:kustomize:ref=Project ProjectRef *KubernetesNameRef `json:"projectRef,omitempty"` // macAddress is the MAC address of the port. diff --git a/api/v1alpha1/project_types.go b/api/v1alpha1/project_types.go index 0c49011c9..7dddf3c0c 100644 --- a/api/v1alpha1/project_types.go +++ b/api/v1alpha1/project_types.go @@ -67,6 +67,7 @@ type ProjectResourceSpec struct { // domainRef is a reference to the ORC Domain which this resource is associated with. // +optional // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="domainRef is immutable" + // +orc:kustomize:ref=Domain DomainRef *KubernetesNameRef `json:"domainRef,omitempty"` // enabled defines whether a project is enabled or not. Default is true. @@ -90,6 +91,7 @@ type ProjectFilter struct { // domainRef is a reference to the ORC Domain which this resource is associated with. // +optional + // +orc:kustomize:ref=Domain DomainRef *KubernetesNameRef `json:"domainRef,omitempty"` FilterByKeystoneTags `json:",inline"` diff --git a/api/v1alpha1/role_types.go b/api/v1alpha1/role_types.go index f4a961dd1..90a8ce5e2 100644 --- a/api/v1alpha1/role_types.go +++ b/api/v1alpha1/role_types.go @@ -32,6 +32,7 @@ type RoleResourceSpec struct { // domainRef is a reference to the ORC Domain which this resource is associated with. // +optional // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="domainRef is immutable" + // +orc:kustomize:ref=Domain DomainRef *KubernetesNameRef `json:"domainRef,omitempty"` } @@ -44,6 +45,7 @@ type RoleFilter struct { // domainRef is a reference to the ORC Domain which this resource is associated with. // +optional + // +orc:kustomize:ref=Domain DomainRef *KubernetesNameRef `json:"domainRef,omitempty"` } diff --git a/api/v1alpha1/roleassignment_types.go b/api/v1alpha1/roleassignment_types.go index b2a947975..7ee3bbb95 100644 --- a/api/v1alpha1/roleassignment_types.go +++ b/api/v1alpha1/roleassignment_types.go @@ -26,26 +26,31 @@ package v1alpha1 type RoleAssignmentResourceSpec struct { // roleRef references the Role being assigned. // +required + // +orc:kustomize:ref=Role RoleRef KubernetesNameRef `json:"roleRef,omitempty"` // userRef references the User receiving the role assignment. // Exactly one of userRef or groupRef must be specified. // +optional + // +orc:kustomize:ref=User UserRef *KubernetesNameRef `json:"userRef,omitempty"` // groupRef references the Group receiving the role assignment. // Exactly one of userRef or groupRef must be specified. // +optional + // +orc:kustomize:ref=Group GroupRef *KubernetesNameRef `json:"groupRef,omitempty"` // projectRef references the Project scope for the assignment. // Exactly one of projectRef or domainRef must be specified. // +optional + // +orc:kustomize:ref=Project ProjectRef *KubernetesNameRef `json:"projectRef,omitempty"` // domainRef references the Domain scope for the assignment. // Exactly one of projectRef or domainRef must be specified. // +optional + // +orc:kustomize:ref=Domain DomainRef *KubernetesNameRef `json:"domainRef,omitempty"` } @@ -54,22 +59,27 @@ type RoleAssignmentResourceSpec struct { type RoleAssignmentFilter struct { // roleRef filters by the referenced Role. // +optional + // +orc:kustomize:ref=Role RoleRef *KubernetesNameRef `json:"roleRef,omitempty"` // userRef filters by the referenced User. // +optional + // +orc:kustomize:ref=User UserRef *KubernetesNameRef `json:"userRef,omitempty"` // groupRef filters by the referenced Group. // +optional + // +orc:kustomize:ref=Group GroupRef *KubernetesNameRef `json:"groupRef,omitempty"` // projectRef filters by the referenced Project scope. // +optional + // +orc:kustomize:ref=Project ProjectRef *KubernetesNameRef `json:"projectRef,omitempty"` // domainRef filters by the referenced Domain scope. // +optional + // +orc:kustomize:ref=Domain DomainRef *KubernetesNameRef `json:"domainRef,omitempty"` } diff --git a/api/v1alpha1/router_interface_types.go b/api/v1alpha1/router_interface_types.go index 2506e5610..e51edd984 100644 --- a/api/v1alpha1/router_interface_types.go +++ b/api/v1alpha1/router_interface_types.go @@ -85,11 +85,13 @@ type RouterInterfaceSpec struct { // routerRef references the router to which this interface belongs. // +required + // +orc:kustomize:ref=Router RouterRef KubernetesNameRef `json:"routerRef,omitempty"` // subnetRef references the subnet the router interface is created on. // +unionMember // +optional + // +orc:kustomize:ref=Subnet SubnetRef *KubernetesNameRef `json:"subnetRef,omitempty"` // resyncPeriod defines how frequently the controller will re-reconcile diff --git a/api/v1alpha1/router_types.go b/api/v1alpha1/router_types.go index 999404a35..f81971739 100644 --- a/api/v1alpha1/router_types.go +++ b/api/v1alpha1/router_types.go @@ -30,6 +30,7 @@ type RouterFilter struct { // projectRef is a reference to the ORC Project this resource is associated with. // Typically, only used by admin. // +optional + // +orc:kustomize:ref=Project ProjectRef *KubernetesNameRef `json:"projectRef,omitempty"` FilterByNeutronTags `json:",inline"` @@ -39,6 +40,7 @@ type ExternalGateway struct { // networkRef is a reference to the ORC Network which the external // gateway is on. // +required + // +orc:kustomize:ref=Network NetworkRef KubernetesNameRef `json:"networkRef,omitempty"` } @@ -95,6 +97,7 @@ type RouterResourceSpec struct { // Typically, only used by admin. // +optional // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="projectRef is immutable" + // +orc:kustomize:ref=Project ProjectRef *KubernetesNameRef `json:"projectRef,omitempty"` } diff --git a/api/v1alpha1/securitygroup_types.go b/api/v1alpha1/securitygroup_types.go index a3f977883..9158dc7fb 100644 --- a/api/v1alpha1/securitygroup_types.go +++ b/api/v1alpha1/securitygroup_types.go @@ -216,6 +216,7 @@ type SecurityGroupResourceSpec struct { // Typically, only used by admin. // +optional // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="projectRef is immutable" + // +orc:kustomize:ref=Project ProjectRef *KubernetesNameRef `json:"projectRef,omitempty"` } @@ -233,6 +234,7 @@ type SecurityGroupFilter struct { // projectRef is a reference to the ORC Project this resource is associated with. // Typically, only used by admin. // +optional + // +orc:kustomize:ref=Project ProjectRef *KubernetesNameRef `json:"projectRef,omitempty"` FilterByNeutronTags `json:",inline"` diff --git a/api/v1alpha1/server_types.go b/api/v1alpha1/server_types.go index af3aa8fee..7ebbc9279 100644 --- a/api/v1alpha1/server_types.go +++ b/api/v1alpha1/server_types.go @@ -57,6 +57,7 @@ type ServerPortSpec struct { // portRef is a reference to a Port object. Server creation will wait for // this port to be created and available. // +optional + // +orc:kustomize:ref=Port PortRef *KubernetesNameRef `json:"portRef,omitempty"` } @@ -66,6 +67,7 @@ type ServerBootVolumeSpec struct { // volumeRef is a reference to a Volume object. The volume must be // bootable (created from an image) and available before server creation. // +required + // +orc:kustomize:ref=Volume VolumeRef KubernetesNameRef `json:"volumeRef,omitempty"` // tag is the device tag applied to the volume. @@ -79,6 +81,7 @@ type ServerVolumeSpec struct { // volumeRef is a reference to a Volume object. Server creation will wait for // this volume to be created and available. // +required + // +orc:kustomize:ref=Volume VolumeRef KubernetesNameRef `json:"volumeRef,omitempty"` // device is the name of the device, such as `/dev/vdb`. @@ -148,11 +151,13 @@ type ServerResourceSpec struct { // This field is required unless bootVolume is specified for boot-from-volume. // +optional // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="imageRef is immutable" + // +orc:kustomize:ref=Image ImageRef *KubernetesNameRef `json:"imageRef,omitempty"` // flavorRef references the flavor to use for the server instance. // +required // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="flavorRef is immutable" + // +orc:kustomize:ref=Flavor FlavorRef KubernetesNameRef `json:"flavorRef,omitempty"` // bootVolume specifies a volume to boot from instead of an image. @@ -191,6 +196,7 @@ type ServerResourceSpec struct { // created with this keypair for SSH access. // +optional // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="keypairRef is immutable" + // +orc:kustomize:ref=KeyPair KeypairRef *KubernetesNameRef `json:"keypairRef,omitempty"` // tags is a list of tags which will be applied to the server. @@ -238,6 +244,7 @@ type ServerSchedulerHints struct { // serverGroupRef is a reference to a ServerGroup object. The server will be // scheduled on a host in the specified server group. // +optional + // +orc:kustomize:ref=ServerGroup ServerGroupRef *KubernetesNameRef `json:"serverGroupRef,omitempty"` // differentHostServerRefs is a list of references to Server objects. @@ -245,6 +252,7 @@ type ServerSchedulerHints struct { // +listType=set // +kubebuilder:validation:MaxItems:=64 // +optional + // +orc:kustomize:ref=Server DifferentHostServerRefs []KubernetesNameRef `json:"differentHostServerRefs,omitempty"` // sameHostServerRefs is a list of references to Server objects. @@ -252,6 +260,7 @@ type ServerSchedulerHints struct { // +listType=set // +kubebuilder:validation:MaxItems:=64 // +optional + // +orc:kustomize:ref=Server SameHostServerRefs []KubernetesNameRef `json:"sameHostServerRefs,omitempty"` // query is a conditional statement that results in compute nodes @@ -289,6 +298,7 @@ type ServerSchedulerHints struct { type UserDataSpec struct { // secretRef is a reference to a Secret containing the user data for this server. // +optional + // +orc:kustomize:ref=Secret SecretRef *KubernetesNameRef `json:"secretRef,omitempty"` } diff --git a/api/v1alpha1/sharenetwork_types.go b/api/v1alpha1/sharenetwork_types.go index 83ef4dfca..61fd7bdd7 100644 --- a/api/v1alpha1/sharenetwork_types.go +++ b/api/v1alpha1/sharenetwork_types.go @@ -35,11 +35,13 @@ type ShareNetworkResourceSpec struct { // networkRef is a reference to the ORC Network which this resource is associated with. // +optional // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="networkRef is immutable" + // +orc:kustomize:ref=Network NetworkRef *KubernetesNameRef `json:"networkRef,omitempty"` // subnetRef is a reference to the ORC Subnet which this resource is associated with. // +optional // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="subnetRef is immutable" + // +orc:kustomize:ref=Subnet SubnetRef *KubernetesNameRef `json:"subnetRef,omitempty"` } diff --git a/api/v1alpha1/subnet_types.go b/api/v1alpha1/subnet_types.go index 1a8450d03..a0334fb92 100644 --- a/api/v1alpha1/subnet_types.go +++ b/api/v1alpha1/subnet_types.go @@ -52,11 +52,13 @@ type SubnetFilter struct { // networkRef is a reference to the ORC Network which this subnet is associated with. // +optional + // +orc:kustomize:ref=Network NetworkRef KubernetesNameRef `json:"networkRef"` // projectRef is a reference to the ORC Project this resource is associated with. // Typically, only used by admin. // +optional + // +orc:kustomize:ref=Project ProjectRef *KubernetesNameRef `json:"projectRef,omitempty"` FilterByNeutronTags `json:",inline"` @@ -74,6 +76,7 @@ type SubnetResourceSpec struct { // networkRef is a reference to the ORC Network which this subnet is associated with. // +required // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="networkRef is immutable" + // +orc:kustomize:ref=Network NetworkRef KubernetesNameRef `json:"networkRef,omitempty"` // tags is a list of tags which will be applied to the subnet. @@ -135,12 +138,14 @@ type SubnetResourceSpec struct { // routerRef specifies a router to attach the subnet to // +optional // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="routerRef is immutable" + // +orc:kustomize:ref=Router RouterRef *KubernetesNameRef `json:"routerRef,omitempty"` // projectRef is a reference to the ORC Project this resource is associated with. // Typically, only used by admin. // +optional // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="projectRef is immutable" + // +orc:kustomize:ref=Project ProjectRef *KubernetesNameRef `json:"projectRef,omitempty"` // TODO: Support service types diff --git a/api/v1alpha1/trunk_types.go b/api/v1alpha1/trunk_types.go index d857c4501..1a638d1c3 100644 --- a/api/v1alpha1/trunk_types.go +++ b/api/v1alpha1/trunk_types.go @@ -21,6 +21,7 @@ package v1alpha1 type TrunkSubportSpec struct { // portRef is a reference to the ORC Port that will be attached as a subport. // +required + // +orc:kustomize:ref=Port PortRef KubernetesNameRef `json:"portRef,omitempty"` // segmentationID is the segmentation ID for the subport (e.g. VLAN ID). @@ -69,11 +70,13 @@ type TrunkResourceSpec struct { // portRef is a reference to the ORC Port which this resource is associated with. // +required // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="portRef is immutable" + // +orc:kustomize:ref=Port PortRef KubernetesNameRef `json:"portRef,omitempty"` // projectRef is a reference to the ORC Project which this resource is associated with. // +optional // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="projectRef is immutable" + // +orc:kustomize:ref=Project ProjectRef *KubernetesNameRef `json:"projectRef,omitempty"` // adminStateUp is the administrative state of the trunk. If false (down), @@ -107,10 +110,12 @@ type TrunkFilter struct { // portRef is a reference to the ORC Port which this resource is associated with. // +optional + // +orc:kustomize:ref=Port PortRef *KubernetesNameRef `json:"portRef,omitempty"` // projectRef is a reference to the ORC Project which this resource is associated with. // +optional + // +orc:kustomize:ref=Project ProjectRef *KubernetesNameRef `json:"projectRef,omitempty"` // Contrary to what the neutron doc say, we can't filter by status diff --git a/api/v1alpha1/user_types.go b/api/v1alpha1/user_types.go index e085006d9..fe33b18d0 100644 --- a/api/v1alpha1/user_types.go +++ b/api/v1alpha1/user_types.go @@ -33,11 +33,13 @@ type UserResourceSpec struct { // domainRef is a reference to the ORC Domain which this resource is associated with. // +optional // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="domainRef is immutable" + // +orc:kustomize:ref=Domain DomainRef *KubernetesNameRef `json:"domainRef,omitempty"` // defaultProjectRef is a reference to the Default Project which this resource is associated with. // +optional // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="defaultProjectRef is immutable" + // +orc:kustomize:ref=Project DefaultProjectRef *KubernetesNameRef `json:"defaultProjectRef,omitempty"` // enabled defines whether a user is enabled or disabled @@ -48,6 +50,7 @@ type UserResourceSpec struct { // for this user. The Secret must contain a key named "password". // If not specified, the user is created without a password. // +optional + // +orc:kustomize:ref=Secret PasswordRef *KubernetesNameRef `json:"passwordRef,omitempty"` } @@ -60,6 +63,7 @@ type UserFilter struct { // domainRef is a reference to the ORC Domain which this resource is associated with. // +optional + // +orc:kustomize:ref=Domain DomainRef *KubernetesNameRef `json:"domainRef,omitempty"` } diff --git a/api/v1alpha1/volume_types.go b/api/v1alpha1/volume_types.go index 49e2f3d06..32b86d806 100644 --- a/api/v1alpha1/volume_types.go +++ b/api/v1alpha1/volume_types.go @@ -40,6 +40,7 @@ type VolumeResourceSpec struct { // volumeTypeRef is a reference to the ORC VolumeType which this resource is associated with. // +optional // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="volumeTypeRef is immutable" + // +orc:kustomize:ref=VolumeType VolumeTypeRef *KubernetesNameRef `json:"volumeTypeRef,omitempty"` // availabilityZone is the availability zone in which to create the volume. @@ -62,6 +63,7 @@ type VolumeResourceSpec struct { // image's min_disk requirement. // +optional // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="imageRef is immutable" + // +orc:kustomize:ref=Image ImageRef *KubernetesNameRef `json:"imageRef,omitempty"` } diff --git a/cmd/kustomizeconfig-generator/main.go b/cmd/kustomizeconfig-generator/main.go new file mode 100644 index 000000000..bba3edbc8 --- /dev/null +++ b/cmd/kustomizeconfig-generator/main.go @@ -0,0 +1,326 @@ +/* +Copyright The ORC Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Command kustomizeconfig-generator generates +// examples/components/kustomizeconfig/kustomizeconfig.yaml from the ORC API +// types. +// +// It discovers cross-references between ORC objects (and core Secrets) by +// scanning api/v1alpha1 for fields annotated with an +// "+orc:kustomize:ref=" marker, then walks the actual (reflected) +// struct layout of every registered ORC Kind's Spec to compute the JSON path +// of each annotated field. The result is emitted as a kustomize Component +// "nameReference" configuration, grouped by referenced Kind. +// +// The "kustomizeref" golangci-lint rule (tools/orc-api-linter) separately +// verifies that every KubernetesNameRef field carries the marker in the +// first place; a missing marker produces no diff here, so `make lint` is +// what catches that case, not this generator. +package main + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "os" + "path/filepath" + "reflect" + "regexp" + "sort" + "strings" + + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/yaml" + + orcv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1" +) + +const ( + apiDir = "api/v1alpha1" + outputFile = "examples/components/kustomizeconfig/kustomizeconfig.yaml" +) + +var markerRE = regexp.MustCompile(`^\+orc:kustomize:ref=(\S+)$`) + +var orcPkgPath = reflect.TypeOf(orcv1alpha1.KubernetesNameRef("")).PkgPath() + +// fieldSpec mirrors kustomize's kustomizeconfig fieldSpec entry. +type fieldSpec struct { + Path string `json:"path"` + Kind string `json:"kind"` +} + +// nameReferenceEntry mirrors kustomize's kustomizeconfig nameReference entry. +type nameReferenceEntry struct { + Kind string `json:"kind"` + FieldSpecs []fieldSpec `json:"fieldSpecs"` +} + +type kustomizeConfig struct { + NameReference []nameReferenceEntry `json:"nameReference"` +} + +// markerTable maps struct type name -> field name -> referenced Kind, as +// declared by "+orc:kustomize:ref=" marker comments in the API source. +type markerTable map[string]map[string]string + +func main() { + if err := run(); err != nil { + fmt.Fprintln(os.Stderr, "kustomizeconfig-generator:", err) + os.Exit(1) + } +} + +func run() error { + markers, err := loadMarkers(apiDir) + if err != nil { + return fmt.Errorf("loading markers from %s: %w", apiDir, err) + } + + scheme := runtime.NewScheme() + if err := orcv1alpha1.AddToScheme(scheme); err != nil { + return fmt.Errorf("building scheme: %w", err) + } + + type hit struct { + sourceKind string + path string + targetKind string + } + var hits []hit + + for gvk, t := range scheme.AllKnownTypes() { + if gvk.Group != orcv1alpha1.GroupName { + continue + } + if strings.HasSuffix(gvk.Kind, "List") { + continue + } + + specField, ok := t.FieldByName("Spec") + if !ok { + continue + } + + sourceKind := gvk.Kind + w := walker{markers: markers} + w.walk(derefStruct(specField.Type), "spec", 0, func(path, targetKind string) { + hits = append(hits, hit{sourceKind: sourceKind, path: path, targetKind: targetKind}) + }) + } + + if len(hits) == 0 { + return fmt.Errorf("no +orc:kustomize:ref markers found; refusing to write an empty kustomizeconfig.yaml") + } + + grouped := map[string][]fieldSpec{} + for _, h := range hits { + grouped[h.targetKind] = append(grouped[h.targetKind], fieldSpec{Path: h.path, Kind: h.sourceKind}) + } + + var targetKinds []string + for k := range grouped { + targetKinds = append(targetKinds, k) + } + sort.Strings(targetKinds) + + cfg := kustomizeConfig{} + for _, k := range targetKinds { + specs := grouped[k] + sort.Slice(specs, func(i, j int) bool { + if specs[i].Kind != specs[j].Kind { + return specs[i].Kind < specs[j].Kind + } + return specs[i].Path < specs[j].Path + }) + // Deduplicate identical (kind, path) pairs which can legitimately + // arise, e.g. the same field appearing in both a create and an + // update variant of a resource spec. + deduped := specs[:0] + for i, s := range specs { + if i > 0 && s == specs[i-1] { + continue + } + deduped = append(deduped, s) + } + cfg.NameReference = append(cfg.NameReference, nameReferenceEntry{Kind: k, FieldSpecs: deduped}) + } + + out, err := yaml.Marshal(cfg) + if err != nil { + return fmt.Errorf("marshalling: %w", err) + } + + header := `# Code generated by kustomizeconfig-generator. DO NOT EDIT. +# +# This file teaches kustomize how to substitute name references between ORC +# objects (and core Secrets) when transforming example manifests, e.g. via a +# nameSuffix or nameReference transformer. It is derived from +# "+orc:kustomize:ref=" markers on the ORC API types. +# +# To add a new reference, annotate the field in api/v1alpha1 with +# "+orc:kustomize:ref=" and run ` + "`make generate`" + `. +# See https://k-orc.cloud/development/api-design/. +` + + return os.WriteFile(outputFile, append([]byte(header), out...), 0o644) +} + +// derefStruct dereferences pointer types until it reaches the underlying type. +func derefStruct(t reflect.Type) reflect.Type { + for t.Kind() == reflect.Ptr { + t = t.Elem() + } + return t +} + +type walker struct { + markers markerTable +} + +// walk recursively descends into struct t, calling record(path, kind) for +// every field annotated with an "+orc:kustomize:ref=" marker. path is +// the JSON path accumulated so far, using "[]" to denote list elements, +// matching kustomize's fieldSpec path syntax. +func (w walker) walk(t reflect.Type, path string, depth int, record func(path, kind string)) { + if depth > 25 { + // Defensive guard; the API has no legitimate structures this deep. + return + } + if t.Kind() != reflect.Struct || t.PkgPath() != orcPkgPath { + return + } + + structName := t.Name() + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + if f.PkgPath != "" { + // unexported field + continue + } + + if f.Anonymous { + // Embedded/inlined field: recurse without adding a path segment. + w.walk(derefStruct(f.Type), path, depth+1, record) + continue + } + + jsonTag := f.Tag.Get("json") + if jsonTag == "" || jsonTag == "-" { + continue + } + name := strings.Split(jsonTag, ",")[0] + if name == "" { + continue + } + fieldPath := path + "/" + name + + ft := derefStruct(f.Type) + isSlice := ft.Kind() == reflect.Slice + + if kind, ok := w.markers[structName][f.Name]; ok { + if isSlice { + fieldPath += "[]" + } + record(fieldPath, kind) + continue + } + + if isSlice { + elem := derefStruct(ft.Elem()) + if elem.Kind() == reflect.Struct && elem.PkgPath() == orcPkgPath { + w.walk(elem, fieldPath+"[]", depth+1, record) + } + continue + } + + if ft.Kind() == reflect.Struct && ft.PkgPath() == orcPkgPath { + w.walk(ft, fieldPath, depth+1, record) + } + } +} + +// loadMarkers scans all Go source files directly under dir for struct field +// doc comments containing "+orc:kustomize:ref=", returning a table +// keyed by struct type name and Go field name. +func loadMarkers(dir string) (markerTable, error) { + table := markerTable{} + + entries, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + + fset := token.NewFileSet() + for _, entry := range entries { + if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") { + continue + } + + filename := filepath.Join(dir, entry.Name()) + file, err := parser.ParseFile(fset, filename, nil, parser.ParseComments) + if err != nil { + return nil, fmt.Errorf("parsing %s: %w", filename, err) + } + + for _, decl := range file.Decls { + genDecl, ok := decl.(*ast.GenDecl) + if !ok || genDecl.Tok != token.TYPE { + continue + } + for _, spec := range genDecl.Specs { + typeSpec, ok := spec.(*ast.TypeSpec) + if !ok { + continue + } + structType, ok := typeSpec.Type.(*ast.StructType) + if !ok { + continue + } + structName := typeSpec.Name.Name + + for _, field := range structType.Fields.List { + if field.Doc == nil { + continue + } + kind := extractRefMarker(field.Doc) + if kind == "" { + continue + } + for _, fieldName := range field.Names { + if table[structName] == nil { + table[structName] = map[string]string{} + } + table[structName][fieldName.Name] = kind + } + } + } + } + } + + return table, nil +} + +func extractRefMarker(doc *ast.CommentGroup) string { + for _, c := range doc.List { + text := strings.TrimSpace(strings.TrimPrefix(c.Text, "//")) + if m := markerRE.FindStringSubmatch(text); m != nil { + return m[1] + } + } + return "" +} diff --git a/cmd/scaffold-controller/data/api/types.go.template b/cmd/scaffold-controller/data/api/types.go.template index 3d3c389b9..913d55f5b 100644 --- a/cmd/scaffold-controller/data/api/types.go.template +++ b/cmd/scaffold-controller/data/api/types.go.template @@ -33,6 +33,7 @@ type {{ .Kind }}ResourceSpec struct { // {{ . | camelCase }}Ref is a reference to the ORC {{ . }} which this resource is associated with. // +required // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="{{ . | camelCase }}Ref is immutable" + // +orc:kustomize:ref={{ . }} {{ . }}Ref KubernetesNameRef `json:"{{ . | camelCase }}Ref,omitempty"` {{- end }} {{- range .OptionalCreateDependencies }} @@ -40,6 +41,7 @@ type {{ .Kind }}ResourceSpec struct { // {{ . | camelCase }}Ref is a reference to the ORC {{ . }} which this resource is associated with. // +optional // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="{{ . | camelCase }}Ref is immutable" + // +orc:kustomize:ref={{ . }} {{ . }}Ref *KubernetesNameRef `json:"{{ . | camelCase }}Ref,omitempty"` {{- end }} @@ -68,6 +70,7 @@ type {{ .Kind }}Filter struct { // {{ . | camelCase }}Ref is a reference to the ORC {{ . }} which this resource is associated with. // +optional + // +orc:kustomize:ref={{ . }} {{ . }}Ref *KubernetesNameRef `json:"{{ . | camelCase }}Ref,omitempty"` {{- end }} diff --git a/examples/components/kustomizeconfig/kustomizeconfig.yaml b/examples/components/kustomizeconfig/kustomizeconfig.yaml index c8439d33b..edb25409c 100644 --- a/examples/components/kustomizeconfig/kustomizeconfig.yaml +++ b/examples/components/kustomizeconfig/kustomizeconfig.yaml @@ -1,104 +1,251 @@ +# Code generated by kustomizeconfig-generator. DO NOT EDIT. +# +# This file teaches kustomize how to substitute name references between ORC +# objects (and core Secrets) when transforming example manifests, e.g. via a +# nameSuffix or nameReference transformer. It is derived from +# "+orc:kustomize:ref=" markers on the ORC API types. +# +# To add a new reference, annotate the field in api/v1alpha1 with +# "+orc:kustomize:ref=" and run `make generate`. +# See https://k-orc.cloud/development/api-design/. nameReference: -- kind: Secret - fieldSpecs: - - path: spec/cloudCredentialsRef/secretName - kind: Flavor - - path: spec/cloudCredentialsRef/secretName - kind: FloatingIP - - path: spec/cloudCredentialsRef/secretName - kind: Image - - path: spec/cloudCredentialsRef/secretName - kind: Network - - path: spec/cloudCredentialsRef/secretName - kind: Port - - path: spec/cloudCredentialsRef/secretName - kind: Project - - path: spec/cloudCredentialsRef/secretName - kind: Router - - path: spec/cloudCredentialsRef/secretName - kind: SecurityGroup - - path: spec/cloudCredentialsRef/secretName - kind: Server - - path: spec/resource/userData/secretRef - kind: Server - - path: spec/cloudCredentialsRef/secretName - kind: Subnet - - path: spec/cloudCredentialsRef/secretName - kind: KeyPair - - path: spec/cloudCredentialsRef/secretName - kind: Trunk - -- kind: Network - fieldSpecs: - - path: spec/resource/networkRef - kind: Subnet - - path: spec/import/filter/networkRef - kind: Subnet - - path: spec/resource/externalGateways/networkRef - kind: Router - - path: spec/resource/networkRef - kind: Port - - path: spec/import/filter/networkRef - kind: Port - - path: spec/resource/floatingNetworkRef - kind: FloatingIP - -- kind: SecurityGroup - fieldSpecs: - - path: spec/resource/securityGroupRefs[] - kind: Port - -- kind: Subnet - fieldSpecs: - - path: spec/resource/addresses[]/subnetRef - kind: Port - -- kind: Router - fieldSpecs: - - path: spec/resource/routerRef - kind: Subnet - -- kind: Image - fieldSpecs: - - path: spec/resource/imageRef - kind: Server - -- kind: Flavor - fieldSpecs: - - path: spec/resource/flavorRef - kind: Server - -- kind: KeyPair - fieldSpecs: - - path: spec/resource/keypairRef - kind: Server - -- kind: Port - fieldSpecs: - - path: spec/resource/portRef - kind: FloatingIP - - path: spec/resource/ports[]/portRef - kind: Server - - path: spec/resource/portRef - kind: Trunk - - path: spec/resource/subports[]/portRef - kind: Trunk - - path: spec/import/filter/portRef - kind: Trunk - -- kind: Project - fieldSpecs: - - path: spec/resource/projectRef - kind: Network - - path: spec/resource/projectRef - kind: Subnet - - path: spec/resource/projectRef - kind: Router - - path: spec/resource/projectRef - kind: Port - - path: spec/resource/projectRef - kind: SecurityGroup - - path: spec/resource/projectRef - kind: Trunk - - path: spec/import/filter/projectRef - kind: Trunk +- fieldSpecs: + - kind: Group + path: spec/import/filter/domainRef + - kind: Group + path: spec/resource/domainRef + - kind: Project + path: spec/import/filter/domainRef + - kind: Project + path: spec/resource/domainRef + - kind: Role + path: spec/import/filter/domainRef + - kind: Role + path: spec/resource/domainRef + - kind: RoleAssignment + path: spec/import/filter/domainRef + - kind: RoleAssignment + path: spec/resource/domainRef + - kind: User + path: spec/import/filter/domainRef + - kind: User + path: spec/resource/domainRef + kind: Domain +- fieldSpecs: + - kind: Server + path: spec/resource/flavorRef + kind: Flavor +- fieldSpecs: + - kind: RoleAssignment + path: spec/import/filter/groupRef + - kind: RoleAssignment + path: spec/resource/groupRef + kind: Group +- fieldSpecs: + - kind: Server + path: spec/resource/imageRef + - kind: Volume + path: spec/resource/imageRef + kind: Image +- fieldSpecs: + - kind: Server + path: spec/resource/keypairRef + kind: KeyPair +- fieldSpecs: + - kind: FloatingIP + path: spec/import/filter/floatingNetworkRef + - kind: FloatingIP + path: spec/resource/floatingNetworkRef + - kind: Port + path: spec/import/filter/networkRef + - kind: Port + path: spec/resource/networkRef + - kind: Router + path: spec/resource/externalGateways[]/networkRef + - kind: ShareNetwork + path: spec/resource/networkRef + - kind: Subnet + path: spec/import/filter/networkRef + - kind: Subnet + path: spec/resource/networkRef + kind: Network +- fieldSpecs: + - kind: FloatingIP + path: spec/import/filter/portRef + - kind: FloatingIP + path: spec/resource/portRef + - kind: Server + path: spec/resource/ports[]/portRef + - kind: Trunk + path: spec/import/filter/portRef + - kind: Trunk + path: spec/resource/portRef + - kind: Trunk + path: spec/resource/subports[]/portRef + kind: Port +- fieldSpecs: + - kind: AddressScope + path: spec/import/filter/projectRef + - kind: AddressScope + path: spec/resource/projectRef + - kind: FloatingIP + path: spec/import/filter/projectRef + - kind: FloatingIP + path: spec/resource/projectRef + - kind: Network + path: spec/import/filter/projectRef + - kind: Network + path: spec/resource/projectRef + - kind: Port + path: spec/import/filter/projectRef + - kind: Port + path: spec/resource/projectRef + - kind: RoleAssignment + path: spec/import/filter/projectRef + - kind: RoleAssignment + path: spec/resource/projectRef + - kind: Router + path: spec/import/filter/projectRef + - kind: Router + path: spec/resource/projectRef + - kind: SecurityGroup + path: spec/import/filter/projectRef + - kind: SecurityGroup + path: spec/resource/projectRef + - kind: Subnet + path: spec/import/filter/projectRef + - kind: Subnet + path: spec/resource/projectRef + - kind: Trunk + path: spec/import/filter/projectRef + - kind: Trunk + path: spec/resource/projectRef + - kind: User + path: spec/resource/defaultProjectRef + kind: Project +- fieldSpecs: + - kind: ApplicationCredential + path: spec/resource/roleRefs[] + - kind: RoleAssignment + path: spec/import/filter/roleRef + - kind: RoleAssignment + path: spec/resource/roleRef + kind: Role +- fieldSpecs: + - kind: RouterInterface + path: spec/routerRef + - kind: Subnet + path: spec/resource/routerRef + kind: Router +- fieldSpecs: + - kind: AddressScope + path: spec/cloudCredentialsRef/secretName + - kind: ApplicationCredential + path: spec/cloudCredentialsRef/secretName + - kind: ApplicationCredential + path: spec/resource/secretRef + - kind: Domain + path: spec/cloudCredentialsRef/secretName + - kind: Endpoint + path: spec/cloudCredentialsRef/secretName + - kind: Flavor + path: spec/cloudCredentialsRef/secretName + - kind: FloatingIP + path: spec/cloudCredentialsRef/secretName + - kind: Group + path: spec/cloudCredentialsRef/secretName + - kind: Image + path: spec/cloudCredentialsRef/secretName + - kind: KeyPair + path: spec/cloudCredentialsRef/secretName + - kind: Network + path: spec/cloudCredentialsRef/secretName + - kind: Port + path: spec/cloudCredentialsRef/secretName + - kind: Project + path: spec/cloudCredentialsRef/secretName + - kind: Role + path: spec/cloudCredentialsRef/secretName + - kind: RoleAssignment + path: spec/cloudCredentialsRef/secretName + - kind: Router + path: spec/cloudCredentialsRef/secretName + - kind: SecurityGroup + path: spec/cloudCredentialsRef/secretName + - kind: Server + path: spec/cloudCredentialsRef/secretName + - kind: Server + path: spec/resource/userData/secretRef + - kind: ServerGroup + path: spec/cloudCredentialsRef/secretName + - kind: Service + path: spec/cloudCredentialsRef/secretName + - kind: ShareNetwork + path: spec/cloudCredentialsRef/secretName + - kind: Subnet + path: spec/cloudCredentialsRef/secretName + - kind: Trunk + path: spec/cloudCredentialsRef/secretName + - kind: User + path: spec/cloudCredentialsRef/secretName + - kind: User + path: spec/resource/passwordRef + - kind: Volume + path: spec/cloudCredentialsRef/secretName + - kind: VolumeType + path: spec/cloudCredentialsRef/secretName + kind: Secret +- fieldSpecs: + - kind: Port + path: spec/resource/securityGroupRefs[] + kind: SecurityGroup +- fieldSpecs: + - kind: Port + path: spec/resource/hostID/serverRef + - kind: Server + path: spec/resource/schedulerHints/differentHostServerRefs[] + - kind: Server + path: spec/resource/schedulerHints/sameHostServerRefs[] + kind: Server +- fieldSpecs: + - kind: Server + path: spec/resource/schedulerHints/serverGroupRef + kind: ServerGroup +- fieldSpecs: + - kind: ApplicationCredential + path: spec/resource/accessRules[]/serviceRef + - kind: Endpoint + path: spec/import/filter/serviceRef + - kind: Endpoint + path: spec/resource/serviceRef + kind: Service +- fieldSpecs: + - kind: FloatingIP + path: spec/resource/floatingSubnetRef + - kind: Port + path: spec/resource/addresses[]/subnetRef + - kind: RouterInterface + path: spec/subnetRef + - kind: ShareNetwork + path: spec/resource/subnetRef + kind: Subnet +- fieldSpecs: + - kind: ApplicationCredential + path: spec/import/filter/userRef + - kind: ApplicationCredential + path: spec/resource/userRef + - kind: RoleAssignment + path: spec/import/filter/userRef + - kind: RoleAssignment + path: spec/resource/userRef + kind: User +- fieldSpecs: + - kind: Server + path: spec/resource/bootVolume/volumeRef + - kind: Server + path: spec/resource/volumes[]/volumeRef + kind: Volume +- fieldSpecs: + - kind: Volume + path: spec/resource/volumeTypeRef + kind: VolumeType diff --git a/tools/orc-api-linter/pkg/analysis/kustomizeref/analyzer.go b/tools/orc-api-linter/pkg/analysis/kustomizeref/analyzer.go new file mode 100644 index 000000000..33ff667b3 --- /dev/null +++ b/tools/orc-api-linter/pkg/analysis/kustomizeref/analyzer.go @@ -0,0 +1,139 @@ +/* +Copyright The ORC Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kustomizeref + +import ( + "go/ast" + "strings" + + "golang.org/x/tools/go/analysis" + "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/extractjsontags" + "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/inspector" + "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers" + "sigs.k8s.io/kube-api-linter/pkg/analysis/initializer" + "sigs.k8s.io/kube-api-linter/pkg/analysis/registry" +) + +const ( + name = "kustomizeref" + + // kustomizeRefMarker is the identifier of the marker which must be + // present on every field which references another ORC object (or a + // core Secret) by KubernetesNameRef, naming the Kind it refers to. + // + // It is consumed by cmd/kustomizeconfig-generator to generate + // examples/components/kustomizeconfig/kustomizeconfig.yaml. + kustomizeRefMarker = "orc:kustomize:ref" + + doc = `Requires every KubernetesNameRef field to carry an +orc:kustomize:ref= marker. + +examples/components/kustomizeconfig/kustomizeconfig.yaml is generated from +these markers (see cmd/kustomizeconfig-generator) so that kustomize can +correctly rewrite cross-object name references (e.g. when a nameSuffix or +nameReference transformer is applied to the examples). A missing marker +produces no diff in the generated file, so this can't be caught by +'make verify-generated' alone. + +Fields declared on a struct whose name contains "Status" are exempt, since +status fields are server-observed and not subject to kustomize name +substitution. + +See: https://k-orc.cloud/development/api-design/` +) + +// Analyzer is the analyzer for the kustomizeref linter. +var Analyzer = &analysis.Analyzer{ + Name: name, + Doc: doc, + Run: run, + Requires: []*analysis.Analyzer{inspector.Analyzer}, +} + +func init() { + registry.DefaultRegistry().RegisterLinter(initializer.NewInitializer( + name, + Analyzer, + false, // not enabled by default - must be explicitly enabled + )) +} + +func run(pass *analysis.Pass) (any, error) { + inspect, ok := pass.ResultOf[inspector.Analyzer].(inspector.Inspector) + if !ok { + return nil, nil + } + + inspect.InspectFieldsIncludingListTypes(func(field *ast.Field, _ extractjsontags.FieldTagInfo, markersAccess markers.Markers, qualifiedFieldName string) { + checkField(pass, field, markersAccess, qualifiedFieldName) + }) + + return nil, nil +} + +func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markers.Markers, qualifiedFieldName string) { + // qualifiedFieldName is in the form "StructName.FieldName" + parts := strings.SplitN(qualifiedFieldName, ".", 2) + if len(parts) != 2 { + return + } + + structName := parts[0] + + // Status fields are server-observed and are never rewritten by + // kustomize name substitution. + if strings.Contains(structName, "Status") { + return + } + + if !isKubernetesNameRefType(field.Type) { + return + } + + if hasKustomizeRefMarker(markersAccess.FieldMarkers(field)) { + return + } + + pass.Reportf(field.Pos(), + "field %s references another object by KubernetesNameRef but has no +%s= marker; "+ + "see https://k-orc.cloud/development/api-design/", + qualifiedFieldName, kustomizeRefMarker) +} + +func hasKustomizeRefMarker(fieldMarkers markers.MarkerSet) bool { + for _, m := range fieldMarkers.Get(kustomizeRefMarker) { + if m.Payload.Value != "" { + return true + } + } + + return false +} + +// isKubernetesNameRefType checks if the expression is KubernetesNameRef, +// *KubernetesNameRef, or []KubernetesNameRef. +func isKubernetesNameRefType(expr ast.Expr) bool { + switch e := expr.(type) { + case *ast.Ident: + return e.Name == "KubernetesNameRef" + case *ast.StarExpr: + return isKubernetesNameRefType(e.X) + case *ast.ArrayType: + return isKubernetesNameRefType(e.Elt) + default: + return false + } +} diff --git a/tools/orc-api-linter/pkg/analysis/kustomizeref/analyzer_test.go b/tools/orc-api-linter/pkg/analysis/kustomizeref/analyzer_test.go new file mode 100644 index 000000000..69dddfc46 --- /dev/null +++ b/tools/orc-api-linter/pkg/analysis/kustomizeref/analyzer_test.go @@ -0,0 +1,28 @@ +/* +Copyright The ORC Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kustomizeref + +import ( + "testing" + + "golang.org/x/tools/go/analysis/analysistest" +) + +func TestAnalyzer(t *testing.T) { + testdata := analysistest.TestData() + analysistest.Run(t, testdata, Analyzer, "a") +} diff --git a/tools/orc-api-linter/pkg/analysis/kustomizeref/doc.go b/tools/orc-api-linter/pkg/analysis/kustomizeref/doc.go new file mode 100644 index 000000000..cb18fb0b8 --- /dev/null +++ b/tools/orc-api-linter/pkg/analysis/kustomizeref/doc.go @@ -0,0 +1,59 @@ +/* +Copyright The ORC Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package kustomizeref provides a linter that ensures every KubernetesNameRef +// field in api/v1alpha1 carries an +orc:kustomize:ref= marker. +// +// # Overview +// +// examples/components/kustomizeconfig/kustomizeconfig.yaml teaches kustomize +// how to rewrite cross-object name references (e.g. under a nameSuffix or +// nameReference transformer) between ORC objects, and between ORC objects and +// core Secrets. It is generated by cmd/kustomizeconfig-generator from +// "+orc:kustomize:ref=" markers on the API types. +// +// # What this linter checks +// +// The linter flags any field typed KubernetesNameRef, *KubernetesNameRef, or +// []KubernetesNameRef which does not carry a "+orc:kustomize:ref=" +// marker naming the Kind it references. +// +// A missing marker produces no diff in the generated kustomizeconfig.yaml +// (the generator simply never learns about the field), so 'make +// verify-generated' alone cannot catch it. This linter closes that gap. +// +// # Examples +// +// Bad (will be flagged): +// +// type ProjectResourceSpec struct { +// DomainRef *KubernetesNameRef `json:"domainRef,omitempty"` +// } +// +// Good (correct pattern): +// +// type ProjectResourceSpec struct { +// // +orc:kustomize:ref=Domain +// DomainRef *KubernetesNameRef `json:"domainRef,omitempty"` +// } +// +// # Status structs are exempt +// +// Fields declared on a struct whose name contains "Status" are exempt, as +// they are server-observed and not subject to kustomize name substitution. +// +// See https://k-orc.cloud/development/api-design/ for more details. +package kustomizeref diff --git a/tools/orc-api-linter/pkg/analysis/kustomizeref/testdata/src/a/a.go b/tools/orc-api-linter/pkg/analysis/kustomizeref/testdata/src/a/a.go new file mode 100644 index 000000000..96cb9368c --- /dev/null +++ b/tools/orc-api-linter/pkg/analysis/kustomizeref/testdata/src/a/a.go @@ -0,0 +1,81 @@ +/* +Copyright The ORC Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package a + +// KubernetesNameRef is a reference to a Kubernetes object by name. +type KubernetesNameRef string + +// ---- Spec structs: missing markers should be flagged ---- + +// ProjectResourceSpec has a marked field and an unmarked field. +type ProjectResourceSpec struct { + // name is fine, not a reference field. + Name *string `json:"name,omitempty"` + + // domainRef is correctly marked. + // +orc:kustomize:ref=Domain + DomainRef *KubernetesNameRef `json:"domainRef,omitempty"` + + // projectRef has no marker and should be flagged. + ProjectRef *KubernetesNameRef `json:"projectRef,omitempty"` // want `field ProjectResourceSpec.ProjectRef references another object by KubernetesNameRef but has no \+orc:kustomize:ref= marker` +} + +// PortResourceSpec tests the non-pointer and slice variants. +type PortResourceSpec struct { + // networkRef is correctly marked and non-pointer. + // +orc:kustomize:ref=Network + NetworkRef KubernetesNameRef `json:"networkRef,omitempty"` + + // securityGroupRefs is correctly marked and a slice. + // +orc:kustomize:ref=SecurityGroup + SecurityGroupRefs []KubernetesNameRef `json:"securityGroupRefs,omitempty"` + + // subnetRef is a slice with no marker and should be flagged. + SubnetRefs []KubernetesNameRef `json:"subnetRefs,omitempty"` // want `field PortResourceSpec.SubnetRefs references another object by KubernetesNameRef but has no \+orc:kustomize:ref= marker` +} + +// ---- Filter structs: missing markers should also be flagged ---- + +// NetworkFilter is a filter struct that should be checked. +type NetworkFilter struct { + // name is fine. + Name *string `json:"name,omitempty"` + + // projectRef has no marker and should be flagged. + ProjectRef *KubernetesNameRef `json:"projectRef,omitempty"` // want `field NetworkFilter.ProjectRef references another object by KubernetesNameRef but has no \+orc:kustomize:ref= marker` +} + +// ---- Status structs: exempt ---- + +// ProjectResourceStatus is a status struct, exempt even without a marker. +type ProjectResourceStatus struct { + // domainRef is allowed without a marker in status. + DomainRef KubernetesNameRef `json:"domainRef,omitempty"` +} + +// ---- Edge cases ---- + +// EmptyMarkerSpec tests that a marker with no value is treated as missing. +type EmptyMarkerSpec struct { + // +orc:kustomize:ref + ProjectRef *KubernetesNameRef `json:"projectRef,omitempty"` // want `field EmptyMarkerSpec.ProjectRef references another object by KubernetesNameRef but has no \+orc:kustomize:ref= marker` +} + +// UnrelatedFieldSpec has non-KubernetesNameRef fields which are never flagged. +type UnrelatedFieldSpec struct { + ProjectID *string `json:"projectID,omitempty"` +} diff --git a/tools/orc-api-linter/plugin.go b/tools/orc-api-linter/plugin.go index d7d0e22bb..c79a2f9a2 100644 --- a/tools/orc-api-linter/plugin.go +++ b/tools/orc-api-linter/plugin.go @@ -29,6 +29,7 @@ import ( _ "sigs.k8s.io/kube-api-linter/pkg/registration" // Import ORC-specific linters to register them with the registry. + _ "github.com/k-orc/openstack-resource-controller/v2/tools/orc-api-linter/pkg/analysis/kustomizeref" _ "github.com/k-orc/openstack-resource-controller/v2/tools/orc-api-linter/pkg/analysis/noopenstackidref" ) diff --git a/website/docs/development/api-design.md b/website/docs/development/api-design.md index 79597cb84..f9caf082c 100644 --- a/website/docs/development/api-design.md +++ b/website/docs/development/api-design.md @@ -55,6 +55,7 @@ There are a number of artifacts which are automatically generated from the API. * The CRDs * The apply configurations in `pkg/clients/applyconfiguration` * Reference documentation for the website +* `examples/components/kustomizeconfig/kustomizeconfig.yaml` Every time you make a change to the API you should ensure these are up to date by running: @@ -64,7 +65,18 @@ make generate ## Kustomize references -You should update `examples/components/kustomizeconfig/kustomizeconfig.yaml` with any references defined by your API. This will ensure that kustomize handles them correctly during transformations. +`examples/components/kustomizeconfig/kustomizeconfig.yaml` is generated by `make generate` (specifically `make generate-kustomizeconfig`, see `cmd/kustomizeconfig-generator`) and must not be edited by hand. + +Whenever you add a field which references another ORC object (or a core `Secret`) using `KubernetesNameRef`, annotate it with a marker naming the referenced Kind: + +```go +// projectRef is a reference to the ORC Project which this resource is associated with. +// +optional +// +orc:kustomize:ref=Project +ProjectRef *KubernetesNameRef `json:"projectRef,omitempty"` +``` + +The generator scans `api/v1alpha1` for `+orc:kustomize:ref=` markers and walks the real struct layout of every registered Kind's `Spec` to compute the JSON path of each annotated field, then emits a kustomize Component `nameReference` configuration grouped by referenced Kind. This ensures that kustomize handles renames of ORC objects (and their credential Secrets) correctly when transforming example manifests. !!! note @@ -84,3 +96,4 @@ You should update `examples/components/kustomizeconfig/kustomizeconfig.yaml` wit * Optional fields should have the `omitempty` tag. * Optional fields should be pointers, unless their zero-value is also the OpenStack default, or we can be very confident that we will never need to distinguish between empty and unset values. e.g. Will we ever want to set a value explicitly to the empty string? * ResourceSpec and Filter fields must reference other ORC objects using `*KubernetesNameRef` with a `Ref` suffix (e.g., `ProjectRef`), not OpenStack resources directly by UUID (e.g., `ProjectID *string`). Exceptions include bare `ID` fields (used for `spec.import.id`) and non-resource IDs like `SegmentationID`. +* Every `KubernetesNameRef` field (and any other field which names another ORC object or a core `Secret`, such as `CloudCredentialsReference.SecretName`) must carry an `+orc:kustomize:ref=` marker. See [Kustomize references](#kustomize-references).