diff --git a/charts/controller/crds/deployments.plural.sh_servicedeployments.yaml b/charts/controller/crds/deployments.plural.sh_servicedeployments.yaml index 54407020df..26ddf449d8 100644 --- a/charts/controller/crds/deployments.plural.sh_servicedeployments.yaml +++ b/charts/controller/crds/deployments.plural.sh_servicedeployments.yaml @@ -313,6 +313,77 @@ spec: description: chart version to use type: string type: object + imports: + items: + properties: + stackRef: + description: |- + ObjectReference contains enough information to let you inspect or modify the referred object. + --- + New uses of this type are discouraged because of difficulty describing its usage when embedded in APIs. + 1. Ignored fields. It includes many fields which are not generally honored. For instance, ResourceVersion and FieldPath are both very rarely valid in actual usage. + 2. Invalid usage help. It is impossible to add specific help for individual usage. In most embedded usages, there are particular + restrictions like, "must refer only to types A and B" or "UID not honored" or "name must be restricted". + Those cannot be well described when embedded. + 3. Inconsistent validation. Because the usages are different, the validation rules are different by usage, which makes it hard for users to predict what will happen. + 4. The fields are both imprecise and overly precise. Kind is not a precise mapping to a URL. This can produce ambiguity + during interpretation and require a REST mapping. In most cases, the dependency is on the group,resource tuple + and the version of the actual struct is irrelevant. + 5. We cannot easily change it. Because this type is embedded in many locations, updates to this type + will affect numerous schemas. Don't make new APIs embed an underspecified API type they do not control. + + + Instead of using this type, create a locally provided and used type that is well-focused on your reference. + For example, ServiceReferences for admission registration: https://github.com/kubernetes/api/blob/release-1.17/admissionregistration/v1/types.go#L533 . + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + TODO: this design is not final and this field is subject to change in the future. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + required: + - stackRef + type: object + type: array + x-kubernetes-validations: + - message: Imports are immutable + rule: self == oldSelf kustomize: properties: path: diff --git a/controller/api/v1alpha1/common_types.go b/controller/api/v1alpha1/common_types.go index 6b2eaf6477..9604281b95 100644 --- a/controller/api/v1alpha1/common_types.go +++ b/controller/api/v1alpha1/common_types.go @@ -148,6 +148,7 @@ func (in *GitRef) Attributes() *console.GitRefAttributes { return &console.GitRefAttributes{ Ref: in.Ref, Folder: in.Folder, + Files: in.Files, } } diff --git a/controller/api/v1alpha1/servicedeployment_types.go b/controller/api/v1alpha1/servicedeployment_types.go index 6ea05dcb0b..7bdf45a3fc 100644 --- a/controller/api/v1alpha1/servicedeployment_types.go +++ b/controller/api/v1alpha1/servicedeployment_types.go @@ -1,6 +1,10 @@ package v1alpha1 import ( + "encoding/json" + + console "github.com/pluralsh/console-client-go" + "github.com/samber/lo" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -23,6 +27,14 @@ type ServiceKustomize struct { Path string `json:"path"` } +func (sk *ServiceKustomize) Attributes() *console.KustomizeAttributes { + if sk == nil { + return nil + } + + return &console.KustomizeAttributes{Path: sk.Path} +} + type ServiceHelm struct { // Fetches the helm values from a secret in this cluster, will consider any key with yaml data a values file and merge them iteratively // +kubebuilder:validation:Optional @@ -68,6 +80,43 @@ type SyncConfigAttributes struct { Annotations map[string]string `json:"annotations,omitempty"` } +func (sca *SyncConfigAttributes) Attributes() (*console.SyncConfigAttributes, error) { + if sca == nil { + return nil, nil + } + + createNamespace := true + if sca.CreateNamespace != nil { + createNamespace = *sca.CreateNamespace + } + + var annotations *string + if sca.Annotations != nil { + result, err := json.Marshal(sca.Annotations) + if err != nil { + return nil, err + } + annotations = lo.ToPtr(string(result)) + } + + var labels *string + if sca.Labels != nil { + result, err := json.Marshal(sca.Labels) + if err != nil { + return nil, err + } + labels = lo.ToPtr(string(result)) + } + + return &console.SyncConfigAttributes{ + CreateNamespace: &createNamespace, + NamespaceMetadata: &console.MetadataAttributes{ + Labels: labels, + Annotations: annotations, + }, + }, nil +} + type ServiceSpec struct { // the name of this service, if not provided ServiceDeployment's own name from ServiceDeployment.ObjectMeta will be used. // +kubebuilder:validation:Optional @@ -109,12 +158,40 @@ type ServiceSpec struct { // Templated should apply liquid templating to raw yaml files, defaults to true // +kubebuilder:validation:Optional Templated *bool `json:"templated,omitempty"` - + // +kubebuilder:validation:Optional + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Imports are immutable" + Imports []ServiceImport `json:"imports"` // Detach determined if user want to delete or detach service // +kubebuilder:validation:Optional Detach bool `json:"detach,omitempty"` } +type ServiceImport struct { + // +kubebuilder:validation:Required + StackRef corev1.ObjectReference `json:"stackRef"` +} + +func (ss *ServiceSpec) DependenciesAttribute() []*console.ServiceDependencyAttributes { + if len(ss.Dependencies) < 1 { + return nil + } + + deps := make([]*console.ServiceDependencyAttributes, 0) + for _, dep := range ss.Dependencies { + deps = append(deps, &console.ServiceDependencyAttributes{Name: dep.Name}) + } + + return deps +} + +func (ss *ServiceSpec) TemplatedAttribute() *bool { + if ss.Templated == nil { + return lo.ToPtr(true) + } + + return ss.Templated +} + type ServiceStatus struct { Status `json:",inline"` diff --git a/controller/config/crd/bases/deployments.plural.sh_servicedeployments.yaml b/controller/config/crd/bases/deployments.plural.sh_servicedeployments.yaml index 54407020df..26ddf449d8 100644 --- a/controller/config/crd/bases/deployments.plural.sh_servicedeployments.yaml +++ b/controller/config/crd/bases/deployments.plural.sh_servicedeployments.yaml @@ -313,6 +313,77 @@ spec: description: chart version to use type: string type: object + imports: + items: + properties: + stackRef: + description: |- + ObjectReference contains enough information to let you inspect or modify the referred object. + --- + New uses of this type are discouraged because of difficulty describing its usage when embedded in APIs. + 1. Ignored fields. It includes many fields which are not generally honored. For instance, ResourceVersion and FieldPath are both very rarely valid in actual usage. + 2. Invalid usage help. It is impossible to add specific help for individual usage. In most embedded usages, there are particular + restrictions like, "must refer only to types A and B" or "UID not honored" or "name must be restricted". + Those cannot be well described when embedded. + 3. Inconsistent validation. Because the usages are different, the validation rules are different by usage, which makes it hard for users to predict what will happen. + 4. The fields are both imprecise and overly precise. Kind is not a precise mapping to a URL. This can produce ambiguity + during interpretation and require a REST mapping. In most cases, the dependency is on the group,resource tuple + and the version of the actual struct is irrelevant. + 5. We cannot easily change it. Because this type is embedded in many locations, updates to this type + will affect numerous schemas. Don't make new APIs embed an underspecified API type they do not control. + + + Instead of using this type, create a locally provided and used type that is well-focused on your reference. + For example, ServiceReferences for admission registration: https://github.com/kubernetes/api/blob/release-1.17/admissionregistration/v1/types.go#L533 . + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + TODO: this design is not final and this field is subject to change in the future. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + required: + - stackRef + type: object + type: array + x-kubernetes-validations: + - message: Imports are immutable + rule: self == oldSelf kustomize: properties: path: diff --git a/controller/config/samples/service_deployment.yaml b/controller/config/samples/service_deployment.yaml index 1de5ef9cfc..b2cdfe0829 100644 --- a/controller/config/samples/service_deployment.yaml +++ b/controller/config/samples/service_deployment.yaml @@ -1,7 +1,52 @@ apiVersion: deployments.plural.sh/v1alpha1 +kind: Cluster +metadata: + name: mgmt + namespace: default +spec: + handle: mgmt +--- +apiVersion: deployments.plural.sh/v1alpha1 +kind: GitRepository +metadata: + name: test + namespace: default +spec: + url: https://github.com/zreigz/tf-hello.git +--- +apiVersion: v1 +kind: Secret +metadata: + name: test-secret + namespace: default +data: + secret: dGVzdA== +--- +apiVersion: deployments.plural.sh/v1alpha1 +kind: InfrastructureStack +metadata: + name: test-stack + namespace: default +spec: + name: "test" + type: TERRAFORM + configuration: + version: "sha-ae2663f-terraform-1.8.2" + image: "ghcr.io/pluralsh/harness" + repositoryRef: + name: test + namespace: default + clusterRef: + name: mgmt + namespace: default + git: + ref: main + folder: terraform +--- +apiVersion: deployments.plural.sh/v1alpha1 kind: ServiceDeployment metadata: - name: k8saws + name: test namespace: default spec: version: 0.0.1 @@ -10,9 +55,14 @@ spec: ref: master repositoryRef: kind: GitRepository - name: k8shelm + name: test namespace: default clusterRef: kind: Cluster - name: aws + name: mgmt namespace: default + imports: + - stackRef: + kind: InfrastructureStack + name: test-stack + namespace: default diff --git a/controller/internal/controller/common.go b/controller/internal/controller/common.go index ffcb6ae018..d7f886c9a1 100644 --- a/controller/internal/controller/common.go +++ b/controller/internal/controller/common.go @@ -75,9 +75,10 @@ func genServiceTemplate(ctx context.Context, c runtimeclient.Client, namespace s } if len(srv.Dependencies) > 0 { serviceTemplate.Dependencies = make([]*console.ServiceDependencyAttributes, 0) - } - for _, dep := range srv.Dependencies { - serviceTemplate.Dependencies = append(serviceTemplate.Dependencies, &console.ServiceDependencyAttributes{Name: dep.Name}) + + for _, dep := range srv.Dependencies { + serviceTemplate.Dependencies = append(serviceTemplate.Dependencies, &console.ServiceDependencyAttributes{Name: dep.Name}) + } } if srv.Templated != nil { diff --git a/controller/internal/controller/servicedeployment_controller.go b/controller/internal/controller/servicedeployment_controller.go index 4b981e79ae..60f15881d9 100644 --- a/controller/internal/controller/servicedeployment_controller.go +++ b/controller/internal/controller/servicedeployment_controller.go @@ -2,7 +2,6 @@ package controller import ( "context" - "encoding/json" "fmt" "sort" @@ -229,6 +228,11 @@ func updateStatus(r *v1alpha1.ServiceDeployment, existingService *console.Servic } func (r *ServiceReconciler) genServiceAttributes(ctx context.Context, service *v1alpha1.ServiceDeployment, repositoryId *string) (*console.ServiceDeploymentAttributes, error) { + syncConfigAttributes, err := service.Spec.SyncConfig.Attributes() + if err != nil { + return nil, err + } + attr := &console.ServiceDeploymentAttributes{ Name: service.ConsoleName(), Namespace: service.ConsoleNamespace(), @@ -236,50 +240,28 @@ func (r *ServiceReconciler) genServiceAttributes(ctx context.Context, service *v DocsPath: service.Spec.DocsPath, Protect: &service.Spec.Protect, RepositoryID: repositoryId, + Git: service.Spec.Git.Attributes(), ContextBindings: make([]*console.ContextBindingAttributes, 0), - Templated: lo.ToPtr(true), - } - - if len(service.Spec.Dependencies) > 0 { - attr.Dependencies = make([]*console.ServiceDependencyAttributes, 0) - } - for _, dep := range service.Spec.Dependencies { - attr.Dependencies = append(attr.Dependencies, &console.ServiceDependencyAttributes{Name: dep.Name}) - } - - if service.Spec.Templated != nil { - attr.Templated = service.Spec.Templated + Templated: service.Spec.TemplatedAttribute(), + Kustomize: service.Spec.Kustomize.Attributes(), + Dependencies: service.Spec.DependenciesAttribute(), + SyncConfig: syncConfigAttributes, } - for _, contextName := range service.Spec.Contexts { - sc, err := r.ConsoleClient.GetServiceContext(contextName) - if err != nil { - return nil, err + if len(service.Spec.Imports) > 0 { + attr.Imports = make([]*console.ServiceImportAttributes, 0) + for _, imp := range service.Spec.Imports { + stackID, err := r.getStackID(ctx, imp.StackRef) + if err != nil { + return nil, err + } + if stackID == nil { + return nil, fmt.Errorf("stack ID is missing") + } + attr.Imports = append(attr.Imports, &console.ServiceImportAttributes{StackID: *stackID}) } - attr.ContextBindings = append(attr.ContextBindings, &console.ContextBindingAttributes{ContextID: sc.ID}) } - if service.Spec.Bindings != nil { - attr.ReadBindings = make([]*console.PolicyBindingAttributes, 0) - attr.WriteBindings = make([]*console.PolicyBindingAttributes, 0) - attr.ReadBindings = algorithms.Map(service.Spec.Bindings.Read, - func(b v1alpha1.Binding) *console.PolicyBindingAttributes { return b.Attributes() }) - attr.WriteBindings = algorithms.Map(service.Spec.Bindings.Write, - func(b v1alpha1.Binding) *console.PolicyBindingAttributes { return b.Attributes() }) - } - - if service.Spec.Kustomize != nil { - attr.Kustomize = &console.KustomizeAttributes{ - Path: service.Spec.Kustomize.Path, - } - } - if service.Spec.Git != nil { - attr.Git = &console.GitRefAttributes{ - Ref: service.Spec.Git.Ref, - Folder: service.Spec.Git.Folder, - Files: service.Spec.Git.Files, - } - } if service.Spec.ConfigurationRef != nil { attr.Configuration = make([]*console.ConfigAttributes, 0) secret := &corev1.Secret{} @@ -296,6 +278,22 @@ func (r *ServiceReconciler) genServiceAttributes(ctx context.Context, service *v }) } } + + for _, contextName := range service.Spec.Contexts { + sc, err := r.ConsoleClient.GetServiceContext(contextName) + if err != nil { + return nil, err + } + attr.ContextBindings = append(attr.ContextBindings, &console.ContextBindingAttributes{ContextID: sc.ID}) + } + + if service.Spec.Bindings != nil { + attr.ReadBindings = algorithms.Map(service.Spec.Bindings.Read, + func(b v1alpha1.Binding) *console.PolicyBindingAttributes { return b.Attributes() }) + attr.WriteBindings = algorithms.Map(service.Spec.Bindings.Write, + func(b v1alpha1.Binding) *console.PolicyBindingAttributes { return b.Attributes() }) + } + if service.Spec.Helm != nil { attr.Helm = &console.HelmConfigAttributes{ Release: service.Spec.Helm.Release, @@ -343,42 +341,21 @@ func (r *ServiceReconciler) genServiceAttributes(ctx context.Context, service *v attr.Helm.Chart = service.Spec.Helm.Chart } } - if service.Spec.SyncConfig != nil { - var annotations *string - var labels *string - createNamespace := true - if service.Spec.SyncConfig.CreateNamespace != nil { - createNamespace = *service.Spec.SyncConfig.CreateNamespace - } - - if service.Spec.SyncConfig.Annotations != nil { - result, err := json.Marshal(service.Spec.SyncConfig.Annotations) - if err != nil { - return nil, err - } - rawAnnotations := string(result) - annotations = &rawAnnotations - } - if service.Spec.SyncConfig.Labels != nil { - result, err := json.Marshal(service.Spec.SyncConfig.Labels) - if err != nil { - return nil, err - } - rawLabels := string(result) - labels = &rawLabels - } - attr.SyncConfig = &console.SyncConfigAttributes{ - CreateNamespace: &createNamespace, - NamespaceMetadata: &console.MetadataAttributes{ - Labels: labels, - Annotations: annotations, - }, - } - } return attr, nil } +func (r *ServiceReconciler) getStackID(ctx context.Context, obj corev1.ObjectReference) (*string, error) { + stack := &v1alpha1.InfrastructureStack{} + if err := r.Get(ctx, client.ObjectKey{Name: obj.Name, Namespace: obj.Namespace}, stack); err != nil { + return nil, err + } + if !stack.Status.HasID() { + return nil, fmt.Errorf("stack is not ready yet") + } + return stack.Status.ID, nil +} + func (r *ServiceReconciler) MergeHelmValues(ctx context.Context, secretRef *corev1.SecretReference, values *runtime.RawExtension) (*string, error) { valuesFromMap := map[string]interface{}{} valuesMap := map[string]interface{}{} @@ -446,6 +423,21 @@ func (r *ServiceReconciler) addOwnerReferences(ctx context.Context, service *v1a } } + if len(service.Spec.Imports) > 0 { + for _, imp := range service.Spec.Imports { + stack := &v1alpha1.InfrastructureStack{} + name := types.NamespacedName{Name: imp.StackRef.Name, Namespace: imp.StackRef.Namespace} + err := r.Get(ctx, name, stack) + if err != nil { + return err + } + err = utils.TryAddOwnerRef(ctx, r.Client, service, stack, r.Scheme) + if err != nil { + return err + } + } + } + return nil } @@ -547,5 +539,6 @@ func (r *ServiceReconciler) SetupWithManager(mgr ctrl.Manager) error { For(&v1alpha1.ServiceDeployment{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). Owns(&corev1.Secret{}, builder.WithPredicates(predicate.ResourceVersionChangedPredicate{})). Owns(&corev1.ConfigMap{}, builder.WithPredicates(predicate.ResourceVersionChangedPredicate{})). + Owns(&v1alpha1.InfrastructureStack{}, builder.WithPredicates(predicate.ResourceVersionChangedPredicate{})). Complete(r) } diff --git a/plural/helm/console/crds/deployments.plural.sh_servicedeployments.yaml b/plural/helm/console/crds/deployments.plural.sh_servicedeployments.yaml index 54407020df..26ddf449d8 100644 --- a/plural/helm/console/crds/deployments.plural.sh_servicedeployments.yaml +++ b/plural/helm/console/crds/deployments.plural.sh_servicedeployments.yaml @@ -313,6 +313,77 @@ spec: description: chart version to use type: string type: object + imports: + items: + properties: + stackRef: + description: |- + ObjectReference contains enough information to let you inspect or modify the referred object. + --- + New uses of this type are discouraged because of difficulty describing its usage when embedded in APIs. + 1. Ignored fields. It includes many fields which are not generally honored. For instance, ResourceVersion and FieldPath are both very rarely valid in actual usage. + 2. Invalid usage help. It is impossible to add specific help for individual usage. In most embedded usages, there are particular + restrictions like, "must refer only to types A and B" or "UID not honored" or "name must be restricted". + Those cannot be well described when embedded. + 3. Inconsistent validation. Because the usages are different, the validation rules are different by usage, which makes it hard for users to predict what will happen. + 4. The fields are both imprecise and overly precise. Kind is not a precise mapping to a URL. This can produce ambiguity + during interpretation and require a REST mapping. In most cases, the dependency is on the group,resource tuple + and the version of the actual struct is irrelevant. + 5. We cannot easily change it. Because this type is embedded in many locations, updates to this type + will affect numerous schemas. Don't make new APIs embed an underspecified API type they do not control. + + + Instead of using this type, create a locally provided and used type that is well-focused on your reference. + For example, ServiceReferences for admission registration: https://github.com/kubernetes/api/blob/release-1.17/admissionregistration/v1/types.go#L533 . + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + TODO: this design is not final and this field is subject to change in the future. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + required: + - stackRef + type: object + type: array + x-kubernetes-validations: + - message: Imports are immutable + rule: self == oldSelf kustomize: properties: path: