diff --git a/services/dis-apim-operator/Makefile b/services/dis-apim-operator/Makefile index 74eda028..cb9d9598 100644 --- a/services/dis-apim-operator/Makefile +++ b/services/dis-apim-operator/Makefile @@ -3,6 +3,8 @@ IMG ?= controller:latest # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. ENVTEST_K8S_VERSION = 1.31.0 +ENABLE_WEBHOOKS ?= false + # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) ifeq (,$(shell go env GOBIN)) GOBIN=$(shell go env GOPATH)/bin @@ -96,7 +98,7 @@ build: manifests generate fmt vet ## Build manager binary. .PHONY: run run: manifests generate fmt vet ## Run a controller from your host. - go run ./cmd/main.go + go run ./cmd/main.go --enable-webhooks=${ENABLE_WEBHOOKS} # If you wish to build the manager image targeting other platforms you can use the --platform flag. # (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it. diff --git a/services/dis-apim-operator/PROJECT b/services/dis-apim-operator/PROJECT index 195804a8..99840efb 100644 --- a/services/dis-apim-operator/PROJECT +++ b/services/dis-apim-operator/PROJECT @@ -7,4 +7,17 @@ layout: - go.kubebuilder.io/v4 projectName: dis-apim-operator repo: github.com/Altinn/altinn-platform/services/dis-apim-operator +resources: +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: dis.altinn.cloud + group: apim + kind: Backend + path: github.com/Altinn/altinn-platform/services/dis-apim-operator/api/v1alpha1 + version: v1alpha1 + webhooks: + defaulting: true + webhookVersion: v1 version: "3" diff --git a/services/dis-apim-operator/api/v1alpha1/backend_types.go b/services/dis-apim-operator/api/v1alpha1/backend_types.go new file mode 100644 index 00000000..3cc4be2e --- /dev/null +++ b/services/dis-apim-operator/api/v1alpha1/backend_types.go @@ -0,0 +1,127 @@ +/* +Copyright 2024 altinn. + +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 v1alpha1 + +import ( + "fmt" + "github.com/Altinn/altinn-platform/services/dis-apim-operator/internal/utils" + apim "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/apimanagement/armapimanagement/v2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// BackendSpec defines the desired state of Backend. +type BackendSpec struct { + //Title - Title of the Backend. May include its purpose, where to get more information, and other relevant information. + //+kubebuilder:validation:Required + Title string `json:"title,omitempty"` + //Description - Description of the Backend. May include its purpose, where to get more information, and other relevant information. + //+kubebuilder:validation:Optional + Description *string `json:"description,omitempty"` + //Url - URL of the Backend. + //+kubebuilder:validation:Required + Url string `json:"url,omitempty"` + //ValidateCertificateChain - Whether to validate the certificate chain when using the backend. + //+kubebuilder:validation:Optional + //+kubebuilder:default:=true + ValidateCertificateChain *bool `json:"validateCertificateChain,omitempty"` + //ValidateCertificateName - Whether to validate the certificate name when using the backend. + //+kubebuilder:validation:Optional + //+kubebuilder:default:=true + ValidateCertificateName *bool `json:"validateCertificateName,omitempty"` + //AzureResourceUidPrefix - The prefix to use for the Azure resource. + //+kubebuilder:validation:Optional + AzureResourcePrefix *string `json:"azureResourceUidPrefix,omitempty"` +} + +// BackendStatus defines the observed state of Backend. +type BackendStatus struct { + //BackendID - The identifier of the Backend. + //+kubebuilder:validation:Optional + BackendID string `json:"backendID,omitempty"` + //ProvisioningState - The provisioning state of the Backend. + //+kubebuilder:validation:Optional + ProvisioningState BackendProvisioningState `json:"provisioningState,omitempty"` + //LastProvisioningError - The last error that occurred during provisioning. + //+kubebuilder:validation:Optional + LastProvisioningError string `json:"lastProvisioningError,omitempty"` +} + +// BackendProvisioningState defines the provisioning state of the Backend. +type BackendProvisioningState string + +const ( + //BackendProvisioningStateSucceeded - The Backend has been successfully provisioned. + BackendProvisioningStateSucceeded BackendProvisioningState = "Succeeded" + //BackendProvisioningStateFailed - The Backend has failed to be provisioned. + BackendProvisioningStateFailed BackendProvisioningState = "Failed" +) + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// Backend is the Schema for the backends API. +type Backend struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec BackendSpec `json:"spec,omitempty"` + Status BackendStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// BackendList contains a list of Backend. +type BackendList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Backend `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Backend{}, &BackendList{}) +} + +// MatchesActualState returns true if the actual state of the resource in azure (apim.BackendContract) matches the desired state defined in the spec. +func (b *Backend) MatchesActualState(actual *apim.BackendClientGetResponse) bool { + return b.Spec.Title == *actual.Properties.Title && + *b.Spec.Description == *actual.Properties.Description && + b.Spec.Url == *actual.Properties.URL && + *b.Spec.ValidateCertificateChain == *actual.Properties.TLS.ValidateCertificateChain && + *b.Spec.ValidateCertificateName == *actual.Properties.TLS.ValidateCertificateName +} + +func (b *Backend) ToAzureBackend() apim.BackendContract { + return apim.BackendContract{ + Properties: &apim.BackendContractProperties{ + Protocol: utils.ToPointer(apim.BackendProtocolHTTP), + URL: utils.ToPointer(b.Spec.Url), + Description: b.Spec.Description, + TLS: &apim.BackendTLSProperties{ + ValidateCertificateChain: b.Spec.ValidateCertificateChain, + ValidateCertificateName: b.Spec.ValidateCertificateName, + }, + Title: utils.ToPointer(b.Spec.Title), + }, + } +} + +func (b *Backend) GetAzureResourceName() string { + if b.Spec.AzureResourcePrefix != nil { + return fmt.Sprintf("%s-%s", *b.Spec.AzureResourcePrefix, b.Name) + } + return fmt.Sprintf("%s-%s", b.Namespace, b.Name) +} diff --git a/services/dis-apim-operator/api/v1alpha1/groupversion_info.go b/services/dis-apim-operator/api/v1alpha1/groupversion_info.go new file mode 100644 index 00000000..8f7faaa6 --- /dev/null +++ b/services/dis-apim-operator/api/v1alpha1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +Copyright 2024 altinn. + +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 v1alpha1 contains API Schema definitions for the apim v1alpha1 API group. +// +kubebuilder:object:generate=true +// +groupName=apim.dis.altinn.cloud +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects. + GroupVersion = schema.GroupVersion{Group: "apim.dis.altinn.cloud", Version: "v1alpha1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme. + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/services/dis-apim-operator/api/v1alpha1/zz_generated.deepcopy.go b/services/dis-apim-operator/api/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 00000000..5f7ad017 --- /dev/null +++ b/services/dis-apim-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,134 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2024 altinn. + +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. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Backend) DeepCopyInto(out *Backend) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Backend. +func (in *Backend) DeepCopy() *Backend { + if in == nil { + return nil + } + out := new(Backend) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Backend) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackendList) DeepCopyInto(out *BackendList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Backend, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendList. +func (in *BackendList) DeepCopy() *BackendList { + if in == nil { + return nil + } + out := new(BackendList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *BackendList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackendSpec) DeepCopyInto(out *BackendSpec) { + *out = *in + if in.Description != nil { + in, out := &in.Description, &out.Description + *out = new(string) + **out = **in + } + if in.ValidateCertificateChain != nil { + in, out := &in.ValidateCertificateChain, &out.ValidateCertificateChain + *out = new(bool) + **out = **in + } + if in.ValidateCertificateName != nil { + in, out := &in.ValidateCertificateName, &out.ValidateCertificateName + *out = new(bool) + **out = **in + } + if in.AzureResourcePrefix != nil { + in, out := &in.AzureResourcePrefix, &out.AzureResourcePrefix + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendSpec. +func (in *BackendSpec) DeepCopy() *BackendSpec { + if in == nil { + return nil + } + out := new(BackendSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackendStatus) DeepCopyInto(out *BackendStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendStatus. +func (in *BackendStatus) DeepCopy() *BackendStatus { + if in == nil { + return nil + } + out := new(BackendStatus) + in.DeepCopyInto(out) + return out +} diff --git a/services/dis-apim-operator/cmd/main.go b/services/dis-apim-operator/cmd/main.go index 4bfaaf3e..ea37eff9 100644 --- a/services/dis-apim-operator/cmd/main.go +++ b/services/dis-apim-operator/cmd/main.go @@ -18,9 +18,13 @@ package main import ( "crypto/tls" - "flag" + "fmt" "os" + "github.com/spf13/pflag" + + "github.com/Altinn/altinn-platform/services/dis-apim-operator/internal/azure" + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" @@ -34,6 +38,11 @@ import ( "sigs.k8s.io/controller-runtime/pkg/metrics/filters" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/webhook" + + apimv1alpha1 "github.com/Altinn/altinn-platform/services/dis-apim-operator/api/v1alpha1" + "github.com/Altinn/altinn-platform/services/dis-apim-operator/internal/config" + "github.com/Altinn/altinn-platform/services/dis-apim-operator/internal/controller" + webhookapimv1alpha1 "github.com/Altinn/altinn-platform/services/dis-apim-operator/internal/webhook/v1alpha1" // +kubebuilder:scaffold:imports ) @@ -45,6 +54,7 @@ var ( func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(apimv1alpha1.AddToScheme(scheme)) // +kubebuilder:scaffold:scheme } @@ -54,25 +64,38 @@ func main() { var probeAddr string var secureMetrics bool var enableHTTP2 bool + var configFile string + var enableWebhooks bool var tlsOpts []func(*tls.Config) - flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ + f := pflag.NewFlagSet("config", pflag.ExitOnError) + f.Usage = func() { + _, _ = fmt.Println(f.FlagUsages()) + os.Exit(0) + } + f.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") - flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") - flag.BoolVar(&enableLeaderElection, "leader-elect", false, + f.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") + f.BoolVar(&enableLeaderElection, "leader-elect", false, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") - flag.BoolVar(&secureMetrics, "metrics-secure", true, + f.BoolVar(&secureMetrics, "metrics-secure", true, "If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.") - flag.BoolVar(&enableHTTP2, "enable-http2", false, + f.BoolVar(&enableHTTP2, "enable-http2", false, "If set, HTTP/2 will be enabled for the metrics and webhook servers") + f.StringVar(&configFile, "config", "dis-apim-config.toml", "Path to toml config file.") + f.BoolVar(&enableWebhooks, "enable-webhooks", true, "Enable webhooks") opts := zap.Options{ Development: true, } - opts.BindFlags(flag.CommandLine) - flag.Parse() + if err := f.Parse(os.Args[1:]); err != nil { + setupLog.Error(err, "failed to parse flags") + os.Exit(1) + } ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + operatorConfig := config.LoadConfigOrDie(configFile, f) + // if the enable-http2 flag is false (the default), http/2 should be disabled // due to its vulnerabilities. More specifically, disabling http/2 will // prevent from being vulnerable to the HTTP/2 Stream Cancellation and @@ -122,7 +145,7 @@ func main() { WebhookServer: webhookServer, HealthProbeBindAddress: probeAddr, LeaderElection: enableLeaderElection, - LeaderElectionID: "02fad30e.dis.altinn.cloud", + LeaderElectionID: "02fad30e.apim.dis.altinn.cloud", // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily // when the Manager ends. This requires the binary to immediately end when the // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly @@ -139,7 +162,25 @@ func main() { setupLog.Error(err, "unable to start manager") os.Exit(1) } - + fmt.Printf("config: %v\n", operatorConfig) + if err = (&controller.BackendReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + NewClient: azure.NewAPIMClient, + ApimClientConfig: &azure.ApimClientConfig{ + AzureConfig: *operatorConfig, + }, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Backend") + os.Exit(1) + } + // nolint:goconst + if enableWebhooks && os.Getenv("ENABLE_WEBHOOKS") != "false" { + if err = webhookapimv1alpha1.SetupBackendWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Backend") + os.Exit(1) + } + } // +kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/services/dis-apim-operator/config/certmanager/certificate.yaml b/services/dis-apim-operator/config/certmanager/certificate.yaml new file mode 100644 index 00000000..43037875 --- /dev/null +++ b/services/dis-apim-operator/config/certmanager/certificate.yaml @@ -0,0 +1,35 @@ +# The following manifests contain a self-signed issuer CR and a certificate CR. +# More document can be found at https://docs.cert-manager.io +# WARNING: Targets CertManager v1.0. Check https://cert-manager.io/docs/installation/upgrading/ for breaking changes. +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + labels: + app.kubernetes.io/name: dis-apim-operator + app.kubernetes.io/managed-by: kustomize + name: selfsigned-issuer + namespace: system +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + labels: + app.kubernetes.io/name: certificate + app.kubernetes.io/instance: serving-cert + app.kubernetes.io/component: certificate + app.kubernetes.io/created-by: dis-apim-operator + app.kubernetes.io/part-of: dis-apim-operator + app.kubernetes.io/managed-by: kustomize + name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml + namespace: system +spec: + # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize + dnsNames: + - SERVICE_NAME.SERVICE_NAMESPACE.svc + - SERVICE_NAME.SERVICE_NAMESPACE.svc.cluster.local + issuerRef: + kind: Issuer + name: selfsigned-issuer + secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize diff --git a/services/dis-apim-operator/config/certmanager/kustomization.yaml b/services/dis-apim-operator/config/certmanager/kustomization.yaml new file mode 100644 index 00000000..bebea5a5 --- /dev/null +++ b/services/dis-apim-operator/config/certmanager/kustomization.yaml @@ -0,0 +1,5 @@ +resources: +- certificate.yaml + +configurations: +- kustomizeconfig.yaml diff --git a/services/dis-apim-operator/config/certmanager/kustomizeconfig.yaml b/services/dis-apim-operator/config/certmanager/kustomizeconfig.yaml new file mode 100644 index 00000000..cf6f89e8 --- /dev/null +++ b/services/dis-apim-operator/config/certmanager/kustomizeconfig.yaml @@ -0,0 +1,8 @@ +# This configuration is for teaching kustomize how to update name ref substitution +nameReference: +- kind: Issuer + group: cert-manager.io + fieldSpecs: + - kind: Certificate + group: cert-manager.io + path: spec/issuerRef/name diff --git a/services/dis-apim-operator/config/crd/bases/apim.dis.altinn.cloud_backends.yaml b/services/dis-apim-operator/config/crd/bases/apim.dis.altinn.cloud_backends.yaml new file mode 100644 index 00000000..b3697d0b --- /dev/null +++ b/services/dis-apim-operator/config/crd/bases/apim.dis.altinn.cloud_backends.yaml @@ -0,0 +1,89 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.4 + name: backends.apim.dis.altinn.cloud +spec: + group: apim.dis.altinn.cloud + names: + kind: Backend + listKind: BackendList + plural: backends + singular: backend + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Backend is the Schema for the backends API. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: BackendSpec defines the desired state of Backend. + properties: + azureResourceUidPrefix: + description: AzureResourceUidPrefix - The prefix to use for the Azure + resource. + type: string + description: + description: Description - Description of the Backend. May include + its purpose, where to get more information, and other relevant information. + type: string + title: + description: Title - Title of the Backend. May include its purpose, + where to get more information, and other relevant information. + type: string + url: + description: Url - URL of the Backend. + type: string + validateCertificateChain: + default: true + description: ValidateCertificateChain - Whether to validate the certificate + chain when using the backend. + type: boolean + validateCertificateName: + default: true + description: ValidateCertificateName - Whether to validate the certificate + name when using the backend. + type: boolean + required: + - title + - url + type: object + status: + description: BackendStatus defines the observed state of Backend. + properties: + backendID: + description: BackendID - The identifier of the Backend. + type: string + lastProvisioningError: + description: LastProvisioningError - The last error that occurred + during provisioning. + type: string + provisioningState: + description: ProvisioningState - The provisioning state of the Backend. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/services/dis-apim-operator/config/crd/kustomization.yaml b/services/dis-apim-operator/config/crd/kustomization.yaml new file mode 100644 index 00000000..ffed2166 --- /dev/null +++ b/services/dis-apim-operator/config/crd/kustomization.yaml @@ -0,0 +1,23 @@ +# This kustomization.yaml is not intended to be run by itself, +# since it depends on service name and namespace that are out of this kustomize package. +# It should be run by config/default +resources: +- bases/apim.dis.altinn.cloud_backends.yaml +# +kubebuilder:scaffold:crdkustomizeresource + +patches: +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. +# patches here are for enabling the conversion webhook for each CRD +- path: patches/webhook_in_backends.yaml +# +kubebuilder:scaffold:crdkustomizewebhookpatch + +# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. +# patches here are for enabling the CA injection for each CRD +#- path: patches/cainjection_in_backends.yaml +# +kubebuilder:scaffold:crdkustomizecainjectionpatch + +# [WEBHOOK] To enable webhook, uncomment the following section +# the following config is for teaching kustomize how to do kustomization for CRDs. + +configurations: +- kustomizeconfig.yaml diff --git a/services/dis-apim-operator/config/crd/kustomizeconfig.yaml b/services/dis-apim-operator/config/crd/kustomizeconfig.yaml new file mode 100644 index 00000000..ec5c150a --- /dev/null +++ b/services/dis-apim-operator/config/crd/kustomizeconfig.yaml @@ -0,0 +1,19 @@ +# This file is for teaching kustomize how to substitute name and namespace reference in CRD +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/name + +namespace: +- kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/namespace + create: false + +varReference: +- path: metadata/annotations diff --git a/services/dis-apim-operator/config/crd/patches/cainjection_in_backends.yaml b/services/dis-apim-operator/config/crd/patches/cainjection_in_backends.yaml new file mode 100644 index 00000000..9d65f4d5 --- /dev/null +++ b/services/dis-apim-operator/config/crd/patches/cainjection_in_backends.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME + name: backends.apim.dis.altinn.cloud diff --git a/services/dis-apim-operator/config/crd/patches/webhook_in_backends.yaml b/services/dis-apim-operator/config/crd/patches/webhook_in_backends.yaml new file mode 100644 index 00000000..aa10a788 --- /dev/null +++ b/services/dis-apim-operator/config/crd/patches/webhook_in_backends.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: backends.apim.dis.altinn.cloud +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/services/dis-apim-operator/config/default/kustomization.yaml b/services/dis-apim-operator/config/default/kustomization.yaml index a68f8f8f..ec1ecfca 100644 --- a/services/dis-apim-operator/config/default/kustomization.yaml +++ b/services/dis-apim-operator/config/default/kustomization.yaml @@ -15,12 +15,12 @@ namePrefix: dis-apim-operator- # someName: someValue resources: -#- ../crd +- ../crd - ../rbac - ../manager # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml -#- ../webhook +- ../webhook # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. #- ../certmanager # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. @@ -43,7 +43,7 @@ patches: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml -#- path: manager_webhook_patch.yaml +- path: manager_webhook_patch.yaml # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. # Uncomment the following replacements to add the cert-manager CA injection annotations diff --git a/services/dis-apim-operator/config/default/manager_webhook_patch.yaml b/services/dis-apim-operator/config/default/manager_webhook_patch.yaml new file mode 100644 index 00000000..365948fc --- /dev/null +++ b/services/dis-apim-operator/config/default/manager_webhook_patch.yaml @@ -0,0 +1,26 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system + labels: + app.kubernetes.io/name: dis-apim-operator + app.kubernetes.io/managed-by: kustomize +spec: + template: + spec: + containers: + - name: manager + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: cert + readOnly: true + volumes: + - name: cert + secret: + defaultMode: 420 + secretName: webhook-server-cert diff --git a/services/dis-apim-operator/config/network-policy/allow-webhook-traffic.yaml b/services/dis-apim-operator/config/network-policy/allow-webhook-traffic.yaml new file mode 100644 index 00000000..e2a14b56 --- /dev/null +++ b/services/dis-apim-operator/config/network-policy/allow-webhook-traffic.yaml @@ -0,0 +1,26 @@ +# This NetworkPolicy allows ingress traffic to your webhook server running +# as part of the controller-manager from specific namespaces and pods. CR(s) which uses webhooks +# will only work when applied in namespaces labeled with 'webhook: enabled' +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + labels: + app.kubernetes.io/name: dis-apim-operator + app.kubernetes.io/managed-by: kustomize + name: allow-webhook-traffic + namespace: system +spec: + podSelector: + matchLabels: + control-plane: controller-manager + policyTypes: + - Ingress + ingress: + # This allows ingress traffic from any namespace with the label webhook: enabled + - from: + - namespaceSelector: + matchLabels: + webhook: enabled # Only from namespaces with this label + ports: + - port: 443 + protocol: TCP diff --git a/services/dis-apim-operator/config/network-policy/kustomization.yaml b/services/dis-apim-operator/config/network-policy/kustomization.yaml index ec0fb5e5..0872bee1 100644 --- a/services/dis-apim-operator/config/network-policy/kustomization.yaml +++ b/services/dis-apim-operator/config/network-policy/kustomization.yaml @@ -1,2 +1,3 @@ resources: +- allow-webhook-traffic.yaml - allow-metrics-traffic.yaml diff --git a/services/dis-apim-operator/config/rbac/backend_editor_role.yaml b/services/dis-apim-operator/config/rbac/backend_editor_role.yaml new file mode 100644 index 00000000..6a184997 --- /dev/null +++ b/services/dis-apim-operator/config/rbac/backend_editor_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to edit backends. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: dis-apim-operator + app.kubernetes.io/managed-by: kustomize + name: backend-editor-role +rules: +- apiGroups: + - apim.dis.altinn.cloud + resources: + - backends + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - apim.dis.altinn.cloud + resources: + - backends/status + verbs: + - get diff --git a/services/dis-apim-operator/config/rbac/backend_viewer_role.yaml b/services/dis-apim-operator/config/rbac/backend_viewer_role.yaml new file mode 100644 index 00000000..4c636091 --- /dev/null +++ b/services/dis-apim-operator/config/rbac/backend_viewer_role.yaml @@ -0,0 +1,23 @@ +# permissions for end users to view backends. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: dis-apim-operator + app.kubernetes.io/managed-by: kustomize + name: backend-viewer-role +rules: +- apiGroups: + - apim.dis.altinn.cloud + resources: + - backends + verbs: + - get + - list + - watch +- apiGroups: + - apim.dis.altinn.cloud + resources: + - backends/status + verbs: + - get diff --git a/services/dis-apim-operator/config/rbac/kustomization.yaml b/services/dis-apim-operator/config/rbac/kustomization.yaml index 5619aa00..653bd92e 100644 --- a/services/dis-apim-operator/config/rbac/kustomization.yaml +++ b/services/dis-apim-operator/config/rbac/kustomization.yaml @@ -18,3 +18,10 @@ resources: - metrics_auth_role.yaml - metrics_auth_role_binding.yaml - metrics_reader_role.yaml +# For each CRD, "Editor" and "Viewer" roles are scaffolded by +# default, aiding admins in cluster management. Those roles are +# not used by the Project itself. You can comment the following lines +# if you do not want those helpers be installed with your Project. +- backend_editor_role.yaml +- backend_viewer_role.yaml + diff --git a/services/dis-apim-operator/config/rbac/role.yaml b/services/dis-apim-operator/config/rbac/role.yaml index 71f095e9..d01aa7e6 100644 --- a/services/dis-apim-operator/config/rbac/role.yaml +++ b/services/dis-apim-operator/config/rbac/role.yaml @@ -1,11 +1,32 @@ +--- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - labels: - app.kubernetes.io/name: dis-apim-operator - app.kubernetes.io/managed-by: kustomize name: manager-role rules: -- apiGroups: [""] - resources: ["pods"] - verbs: ["get", "list", "watch"] +- apiGroups: + - apim.dis.altinn.cloud + resources: + - backends + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - apim.dis.altinn.cloud + resources: + - backends/finalizers + verbs: + - update +- apiGroups: + - apim.dis.altinn.cloud + resources: + - backends/status + verbs: + - get + - patch + - update diff --git a/services/dis-apim-operator/config/samples/apim_v1alpha1_backend.ignore.yaml b/services/dis-apim-operator/config/samples/apim_v1alpha1_backend.ignore.yaml new file mode 100644 index 00000000..c4689d89 --- /dev/null +++ b/services/dis-apim-operator/config/samples/apim_v1alpha1_backend.ignore.yaml @@ -0,0 +1,12 @@ +apiVersion: apim.dis.altinn.cloud/v1alpha1 +kind: Backend +metadata: + labels: + app.kubernetes.io/name: dis-apim-operator + app.kubernetes.io/managed-by: kustomize + name: backend-sample +spec: + title: test-backend + description: test-backend + url: https://primary-test-aca-vga.ambitioushill-7fbb0e9c.norwayeast.azurecontainerapps.io + azureResourceUidPrefix: test \ No newline at end of file diff --git a/services/dis-apim-operator/config/samples/apim_v1alpha1_backend.yaml b/services/dis-apim-operator/config/samples/apim_v1alpha1_backend.yaml new file mode 100644 index 00000000..ccf2e4aa --- /dev/null +++ b/services/dis-apim-operator/config/samples/apim_v1alpha1_backend.yaml @@ -0,0 +1,9 @@ +apiVersion: apim.dis.altinn.cloud/v1alpha1 +kind: Backend +metadata: + labels: + app.kubernetes.io/name: dis-apim-operator + app.kubernetes.io/managed-by: kustomize + name: backend-sample +spec: + diff --git a/services/dis-apim-operator/config/samples/kustomization.yaml b/services/dis-apim-operator/config/samples/kustomization.yaml new file mode 100644 index 00000000..364efc2f --- /dev/null +++ b/services/dis-apim-operator/config/samples/kustomization.yaml @@ -0,0 +1,4 @@ +## Append samples of your project ## +resources: +- apim_v1alpha1_backend.yaml +# +kubebuilder:scaffold:manifestskustomizesamples diff --git a/services/dis-apim-operator/config/webhook/kustomization.yaml b/services/dis-apim-operator/config/webhook/kustomization.yaml new file mode 100644 index 00000000..9cf26134 --- /dev/null +++ b/services/dis-apim-operator/config/webhook/kustomization.yaml @@ -0,0 +1,6 @@ +resources: +- manifests.yaml +- service.yaml + +configurations: +- kustomizeconfig.yaml diff --git a/services/dis-apim-operator/config/webhook/kustomizeconfig.yaml b/services/dis-apim-operator/config/webhook/kustomizeconfig.yaml new file mode 100644 index 00000000..206316e5 --- /dev/null +++ b/services/dis-apim-operator/config/webhook/kustomizeconfig.yaml @@ -0,0 +1,22 @@ +# the following config is for teaching kustomize where to look at when substituting nameReference. +# It requires kustomize v2.1.0 or newer to work properly. +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: MutatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/name + - kind: ValidatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/name + +namespace: +- kind: MutatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/namespace + create: true +- kind: ValidatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/namespace + create: true diff --git a/services/dis-apim-operator/config/webhook/manifests.yaml b/services/dis-apim-operator/config/webhook/manifests.yaml new file mode 100644 index 00000000..60a6f311 --- /dev/null +++ b/services/dis-apim-operator/config/webhook/manifests.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: mutating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-apim-dis-altinn-cloud-v1alpha1-backend + failurePolicy: Fail + name: mbackend-v1alpha1.kb.io + rules: + - apiGroups: + - apim.dis.altinn.cloud + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - backends + sideEffects: None diff --git a/services/dis-apim-operator/config/webhook/service.yaml b/services/dis-apim-operator/config/webhook/service.yaml new file mode 100644 index 00000000..2dc8c0c9 --- /dev/null +++ b/services/dis-apim-operator/config/webhook/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/name: dis-apim-operator + app.kubernetes.io/managed-by: kustomize + name: webhook-service + namespace: system +spec: + ports: + - port: 443 + protocol: TCP + targetPort: 9443 + selector: + control-plane: controller-manager diff --git a/services/dis-apim-operator/go.mod b/services/dis-apim-operator/go.mod index 2f8c10b4..d972f51c 100644 --- a/services/dis-apim-operator/go.mod +++ b/services/dis-apim-operator/go.mod @@ -3,14 +3,26 @@ module github.com/Altinn/altinn-platform/services/dis-apim-operator go 1.22.0 require ( + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/apimanagement/armapimanagement/v2 v2.1.0 + github.com/knadh/koanf/parsers/toml/v2 v2.1.0 + github.com/knadh/koanf/providers/env v1.0.0 + github.com/knadh/koanf/providers/file v1.1.2 + github.com/knadh/koanf/providers/posflag v0.1.0 + github.com/knadh/koanf/v2 v2.1.1 github.com/onsi/ginkgo/v2 v2.19.0 github.com/onsi/gomega v1.33.1 + github.com/spf13/pflag v1.0.5 + k8s.io/api v0.31.0 k8s.io/apimachinery v0.31.0 k8s.io/client-go v0.31.0 sigs.k8s.io/controller-runtime v0.19.0 ) require ( + github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -30,7 +42,9 @@ require ( github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.4 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v5 v5.0.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/cel-go v0.20.1 // indirect @@ -44,17 +58,22 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/knadh/koanf/maps v0.1.1 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.19.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/spf13/cobra v1.8.1 // indirect - github.com/spf13/pflag v1.0.5 // indirect github.com/stoewer/go-strcase v1.2.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect @@ -67,6 +86,7 @@ require ( go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect + golang.org/x/crypto v0.24.0 // indirect golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect golang.org/x/net v0.26.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect @@ -84,7 +104,6 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.31.0 // indirect k8s.io/apiextensions-apiserver v0.31.0 // indirect k8s.io/apiserver v0.31.0 // indirect k8s.io/component-base v0.31.0 // indirect diff --git a/services/dis-apim-operator/go.sum b/services/dis-apim-operator/go.sum index a8ec01da..4ef51867 100644 --- a/services/dis-apim-operator/go.sum +++ b/services/dis-apim-operator/go.sum @@ -1,3 +1,17 @@ +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0 h1:fb8kj/Dh4CSwgsOzHeZY4Xh68cFVbzXx+ONXGMY//4w= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0/go.mod h1:uReU2sSxZExRPBAg3qKzmAucSi51+SP1OhohieR821Q= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 h1:BMAjVKJM0U/CYF27gA0ZMmXGkOcvfFtD0oHVZ1TIPRI= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0/go.mod h1:1fXstnBMas5kzG+S3q8UoJcmyU6nUeunJcMDHcRYHhs= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.0 h1:d81/ng9rET2YqdVkVwkb6EXeRrLJIwyGnJcAlAWKwhs= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.0/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/apimanagement/armapimanagement/v2 v2.1.0 h1:WYADp5XlioccEnBBK9sVUaHVno76l7WeTcWCumN86kM= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/apimanagement/armapimanagement/v2 v2.1.0/go.mod h1:PK8v1aAd2Wx6eTcbUYhYstGpspqNqhZYiM8GLFdq2A0= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0 h1:PTFGRSlMKCQelWwxUyYVEUqseBJVemLyqWJjvMyt0do= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0/go.mod h1:LRr2FzBTQlONPPa5HREE5+RjSCTXl7BwOvYOaWTqCaI= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1 h1:7CBQ+Ei8SP2c6ydQTGCCrS35bDxgTMfoP2miAwK++OU= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1/go.mod h1:c/wcGeGx5FUPbM/JltUYHZcKmigwyVLJlDq+4HdtXaw= +github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 h1:WpB/QDNLpMw72xHJc34BNNykqSOeEJDAWkhf0u12/Jk= +github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= @@ -16,6 +30,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= @@ -44,8 +60,12 @@ github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogB github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c= +github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -76,6 +96,18 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= +github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/parsers/toml/v2 v2.1.0 h1:EUdIKIeezfDj6e1ABDhIjhbURUpyrP1HToqW6tz8R0I= +github.com/knadh/koanf/parsers/toml/v2 v2.1.0/go.mod h1:0KtwfsWJt4igUTQnsn0ZjFWVrP80Jv7edTBRbQFd2ho= +github.com/knadh/koanf/providers/env v1.0.0 h1:ufePaI9BnWH+ajuxGGiJ8pdTG0uLEUWC7/HDDPGLah0= +github.com/knadh/koanf/providers/env v1.0.0/go.mod h1:mzFyRZueYhb37oPmC1HAv/oGEEuyvJDA98r3XAa8Gak= +github.com/knadh/koanf/providers/file v1.1.2 h1:aCC36YGOgV5lTtAFz2qkgtWdeQsgfxUkxDOe+2nQY3w= +github.com/knadh/koanf/providers/file v1.1.2/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI= +github.com/knadh/koanf/providers/posflag v0.1.0 h1:mKJlLrKPcAP7Ootf4pBZWJ6J+4wHYujwipe7Ie3qW6U= +github.com/knadh/koanf/providers/posflag v0.1.0/go.mod h1:SYg03v/t8ISBNrMBRMlojH8OsKowbkXV7giIbBVgbz0= +github.com/knadh/koanf/v2 v2.1.1 h1:/R8eXqasSTsmDCsAyYj+81Wteg8AqrV9CP6gvsTsOmM= +github.com/knadh/koanf/v2 v2.1.1/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -83,8 +115,14 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -96,6 +134,10 @@ github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -121,11 +163,13 @@ github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= @@ -157,6 +201,8 @@ go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU= golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -177,6 +223,7 @@ golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= diff --git a/services/dis-apim-operator/internal/azure/apim_client.go b/services/dis-apim-operator/internal/azure/apim_client.go new file mode 100644 index 00000000..1473ad3c --- /dev/null +++ b/services/dis-apim-operator/internal/azure/apim_client.go @@ -0,0 +1,118 @@ +package azure + +import ( + "context" + "errors" + "github.com/Altinn/altinn-platform/services/dis-apim-operator/internal/config" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + apim "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/apimanagement/armapimanagement/v2" + "net/http" +) + +// APIMClient is a client for interacting with the Azure API Management service +type APIMClient struct { + // ApimClientConfig is the configuration for the APIM client + ApimClientConfig ApimClientConfig + apimClientFactory *apim.ClientFactory +} + +// ApimClientConfig is the configuration for the APIMClient +type ApimClientConfig struct { + config.AzureConfig `json:",inline"` + ClientOptions *azidentity.DefaultAzureCredentialOptions `json:"clientOptions,omitempty"` + FactoryOptions *arm.ClientOptions `json:"factoryOptions,omitempty"` +} + +// NewAPIMClient creates a new APIMClient +func NewAPIMClient(config *ApimClientConfig) (*APIMClient, error) { + credential, err := azidentity.NewDefaultAzureCredential(config.ClientOptions) + if err != nil { + return nil, err + } + clientFactory, err := apim.NewClientFactory(config.SubscriptionId, credential, config.FactoryOptions) + if err != nil { + return nil, err + } + return &APIMClient{ + ApimClientConfig: *config, + apimClientFactory: clientFactory, + }, nil +} + +func (c *APIMClient) GetApiVersionSet(ctx context.Context, apiVersionSetName string, options *apim.APIVersionSetClientGetOptions) (apim.APIVersionSetClientGetResponse, error) { + client := c.apimClientFactory.NewAPIVersionSetClient() + return client.Get(ctx, c.ApimClientConfig.ResourceGroup, c.ApimClientConfig.ApimServiceName, apiVersionSetName, options) +} + +func (c *APIMClient) CreateUpdateApiVersionSet(ctx context.Context, apiVersionSetName string, parameters apim.APIVersionSetContract, options *apim.APIVersionSetClientCreateOrUpdateOptions) (apim.APIVersionSetClientCreateOrUpdateResponse, error) { + client := c.apimClientFactory.NewAPIVersionSetClient() + return client.CreateOrUpdate(ctx, c.ApimClientConfig.ResourceGroup, c.ApimClientConfig.ApimServiceName, apiVersionSetName, parameters, options) +} + +func (c *APIMClient) DeleteApiVersionSet(ctx context.Context, apiVersionSetName string, etag string, options *apim.APIVersionSetClientDeleteOptions) (apim.APIVersionSetClientDeleteResponse, error) { + client := c.apimClientFactory.NewAPIVersionSetClient() + return client.Delete(ctx, c.ApimClientConfig.ResourceGroup, c.ApimClientConfig.ApimServiceName, apiVersionSetName, etag, options) +} + +func (c *APIMClient) GetApi(ctx context.Context, apiId string, options *apim.APIClientGetOptions) (apim.APIClientGetResponse, error) { + client := c.apimClientFactory.NewAPIClient() + return client.Get(ctx, c.ApimClientConfig.ResourceGroup, c.ApimClientConfig.ApimServiceName, apiId, options) +} + +func (c *APIMClient) CreateUpdateApi(ctx context.Context, apiId string, parameters apim.APICreateOrUpdateParameter, options *apim.APIClientBeginCreateOrUpdateOptions) (*runtime.Poller[apim.APIClientCreateOrUpdateResponse], error) { + client := c.apimClientFactory.NewAPIClient() + return client.BeginCreateOrUpdate(ctx, c.ApimClientConfig.ResourceGroup, c.ApimClientConfig.ApimServiceName, apiId, parameters, options) +} + +func (c *APIMClient) DeleteApi(ctx context.Context, apiId string, etag string, options *apim.APIClientDeleteOptions) (apim.APIClientDeleteResponse, error) { + client := c.apimClientFactory.NewAPIClient() + return client.Delete(ctx, c.ApimClientConfig.ResourceGroup, c.ApimClientConfig.ApimServiceName, apiId, etag, options) +} + +func (c *APIMClient) GetApiPolicy(ctx context.Context, apiId string, options *apim.APIPolicyClientGetOptions) (apim.APIPolicyClientGetResponse, error) { + client := c.apimClientFactory.NewAPIPolicyClient() + return client.Get(ctx, c.ApimClientConfig.ResourceGroup, c.ApimClientConfig.ApimServiceName, apiId, apim.PolicyIDNamePolicy, options) +} + +func (c *APIMClient) CreateUpdateApiPolicy(ctx context.Context, apiId string, parameters apim.PolicyContract, options *apim.APIPolicyClientCreateOrUpdateOptions) (apim.APIPolicyClientCreateOrUpdateResponse, error) { + client := c.apimClientFactory.NewAPIPolicyClient() + return client.CreateOrUpdate(ctx, c.ApimClientConfig.ResourceGroup, c.ApimClientConfig.ApimServiceName, apiId, apim.PolicyIDNamePolicy, parameters, options) +} + +func (c *APIMClient) DeleteApiPolicy(ctx context.Context, apiId string, etag string, options *apim.APIPolicyClientDeleteOptions) (apim.APIPolicyClientDeleteResponse, error) { + client := c.apimClientFactory.NewAPIPolicyClient() + return client.Delete(ctx, c.ApimClientConfig.ResourceGroup, c.ApimClientConfig.ApimServiceName, apiId, apim.PolicyIDNamePolicy, etag, options) +} + +func (c *APIMClient) GetBackend(ctx context.Context, backendId string, options *apim.BackendClientGetOptions) (apim.BackendClientGetResponse, error) { + client := c.apimClientFactory.NewBackendClient() + return client.Get(ctx, c.ApimClientConfig.ResourceGroup, c.ApimClientConfig.ApimServiceName, backendId, options) +} + +func (c *APIMClient) CreateUpdateBackend(ctx context.Context, backendId string, parameters apim.BackendContract, options *apim.BackendClientCreateOrUpdateOptions) (apim.BackendClientCreateOrUpdateResponse, error) { + client := c.apimClientFactory.NewBackendClient() + return client.CreateOrUpdate(ctx, c.ApimClientConfig.ResourceGroup, c.ApimClientConfig.ApimServiceName, backendId, parameters, options) +} + +func (c *APIMClient) DeleteBackend(ctx context.Context, backendId string, etag string, options *apim.BackendClientDeleteOptions) (apim.BackendClientDeleteResponse, error) { + client := c.apimClientFactory.NewBackendClient() + return client.Delete(ctx, c.ApimClientConfig.ResourceGroup, c.ApimClientConfig.ApimServiceName, backendId, etag, options) +} + +func IsNotFoundError(err error) bool { + var responseError *azcore.ResponseError + if errors.As(err, &responseError) { + return responseError.StatusCode == http.StatusNotFound + } + return false +} + +func IgnoreNotFound(err error) error { + if IsNotFoundError(err) { + return nil + } + return err +} diff --git a/services/dis-apim-operator/internal/config/conf.go b/services/dis-apim-operator/internal/config/conf.go new file mode 100644 index 00000000..098590bd --- /dev/null +++ b/services/dis-apim-operator/internal/config/conf.go @@ -0,0 +1,62 @@ +package config + +import ( + "fmt" + "github.com/knadh/koanf/parsers/toml/v2" + "github.com/knadh/koanf/providers/env" + "github.com/knadh/koanf/providers/file" + "github.com/knadh/koanf/providers/posflag" + "github.com/knadh/koanf/v2" + "github.com/spf13/pflag" + "os" + "strings" +) + +type AzureConfig struct { + SubscriptionId string `json:"subscriptionId,omitempty" koanf:"subscriptionId" toml:"subscriptionId"` + ResourceGroup string `json:"resourceGroup,omitempty" koanf:"resourceGroup" toml:"resourceGroup"` + ApimServiceName string `json:"apimServiceName,omitempty" koanf:"apimServiceName" toml:"apimServiceName"` +} + +func LoadConfig(configFile string, flagset *pflag.FlagSet) (*AzureConfig, error) { + k := koanf.New(".") + + // Load from file + if configFile != "" { + if _, err := os.Stat(configFile); err == nil { + err := k.Load(file.Provider(configFile), toml.Parser()) + if err != nil { + return nil, fmt.Errorf("error loading config file: %w", err) + } + } + } + + // Load from environment + err := k.Load(env.Provider("DISAPIM_", ".", func(s string) string { + return strings.Replace(strings.ToLower( + strings.TrimPrefix(s, "DISAPIM_")), "_", ".", -1) + }), nil) + + if err != nil { + return nil, fmt.Errorf("error loading environment variables: %w", err) + } + + // Load from flags + err = k.Load(posflag.Provider(flagset, ".", k), nil) + if err != nil { + return nil, fmt.Errorf("error loading flags: %w", err) + } + var c AzureConfig + if err := k.Unmarshal("", &c); err != nil { + return nil, fmt.Errorf("error unmarshalling config: %w", err) + } + return &c, nil +} + +func LoadConfigOrDie(configFile string, flagset *pflag.FlagSet) *AzureConfig { + c, err := LoadConfig(configFile, flagset) + if err != nil { + panic(err) + } + return c +} diff --git a/services/dis-apim-operator/internal/controller/backend_controller.go b/services/dis-apim-operator/internal/controller/backend_controller.go new file mode 100644 index 00000000..2a28ac2f --- /dev/null +++ b/services/dis-apim-operator/internal/controller/backend_controller.go @@ -0,0 +1,168 @@ +/* +Copyright 2024 altinn. + +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 controller + +import ( + "context" + "fmt" + "github.com/Altinn/altinn-platform/services/dis-apim-operator/internal/azure" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "time" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + apimv1alpha1 "github.com/Altinn/altinn-platform/services/dis-apim-operator/api/v1alpha1" +) + +const BACKEND_FINALIZER = "finalizers.apim.dis.altinn.cloud/backend" + +type newApimClient func(config *azure.ApimClientConfig) (*azure.APIMClient, error) + +// BackendReconciler reconciles a Backend object +type BackendReconciler struct { + client.Client + Scheme *runtime.Scheme + ApimClientConfig *azure.ApimClientConfig + NewClient newApimClient + apimClient *azure.APIMClient +} + +// +kubebuilder:rbac:groups=apim.dis.altinn.cloud,resources=backends,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=apim.dis.altinn.cloud,resources=backends/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=apim.dis.altinn.cloud,resources=backends/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the Backend object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.0/pkg/reconcile +func (r *BackendReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx) + + var backend apimv1alpha1.Backend + if err := r.Get(ctx, req.NamespacedName, &backend); err != nil { + if client.IgnoreNotFound(err) != nil { + logger.Error(err, "unable to fetch Backend") + return ctrl.Result{}, err + } + // Object not found, return and don't requeue + return ctrl.Result{}, nil + } + + if !controllerutil.ContainsFinalizer(&backend, BACKEND_FINALIZER) { + controllerutil.AddFinalizer(&backend, BACKEND_FINALIZER) + if err := r.Update(ctx, &backend); err != nil { + logger.Error(err, "Failed to add finalizer") + return ctrl.Result{}, err + } + } + c, err := r.NewClient(r.ApimClientConfig) + if err != nil { + logger.Error(err, "Failed to create APIM client") + return ctrl.Result{}, err + } + r.apimClient = c + if backend.DeletionTimestamp != nil { + return ctrl.Result{}, r.handleDeletion(ctx, &backend) + } + azBackend, err := r.apimClient.GetBackend(ctx, backend.GetAzureResourceName(), nil) + if err != nil { + if azure.IsNotFoundError(err) { + if err := r.handleCreateUpdate(ctx, &backend); err != nil { + logger.Error(err, "Failed to create backend") + return ctrl.Result{}, err + } + logger.Info("Backend created") + return ctrl.Result{RequeueAfter: 1 * time.Minute}, nil + } + logger.Error(err, "Failed to get backend") + return ctrl.Result{}, err + } + if !backend.MatchesActualState(&azBackend) { + logger.Info("Backend does not match actual state, updating") + err := r.handleCreateUpdate(ctx, &backend) + if err != nil { + logger.Error(err, "Failed to update backend") + return ctrl.Result{}, err + } + logger.Info("Backend updated") + return ctrl.Result{RequeueAfter: 1 * time.Minute}, nil + } + return ctrl.Result{RequeueAfter: 1 * time.Minute}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *BackendReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&apimv1alpha1.Backend{}). + Named("backend"). + Complete(r) +} + +func (r *BackendReconciler) handleCreateUpdate(ctx context.Context, backend *apimv1alpha1.Backend) error { + res, err := r.apimClient.CreateUpdateBackend(ctx, backend.GetAzureResourceName(), backend.ToAzureBackend(), nil) + if err != nil { + backend.Status.ProvisioningState = apimv1alpha1.BackendProvisioningStateFailed + backend.Status.LastProvisioningError = fmt.Sprintf("err when creating backend: %v", err) + if errUpdate := r.Status().Update(ctx, backend); errUpdate != nil { + return fmt.Errorf("failed to update status to failed: %v", errUpdate) + } + return err + } + backend.Status.BackendID = *res.ID + backend.Status.ProvisioningState = apimv1alpha1.BackendProvisioningStateSucceeded + if errUpdate := r.Status().Update(ctx, backend); errUpdate != nil { + return fmt.Errorf("failed to update status to succeeded: %v", errUpdate) + } + return nil +} + +func (r *BackendReconciler) handleDeletion(ctx context.Context, backend *apimv1alpha1.Backend) error { + logger := log.FromContext(ctx) + azureBackend, err := r.apimClient.GetBackend(ctx, backend.GetAzureResourceName(), nil) + if err != nil { + if azure.IsNotFoundError(err) { + controllerutil.RemoveFinalizer(backend, BACKEND_FINALIZER) + if err := r.Update(ctx, backend); err != nil { + logger.Error(err, "Failed to remove finalizer") + return err + } + return nil + } + logger.Error(err, "Failed to get backend for deletion") + return err + } + resp, err := r.apimClient.DeleteBackend(ctx, backend.GetAzureResourceName(), *azureBackend.ETag, nil) + if err != nil { + logger.Error(err, fmt.Sprintf("Failed to delete backend. backend: %#v", azureBackend)) + return err + } + logger.Info("Backend deleted", "response", resp) + controllerutil.RemoveFinalizer(backend, BACKEND_FINALIZER) + if err := r.Update(ctx, backend); err != nil { + logger.Error(err, "Failed to remove finalizer") + return err + } + return nil +} diff --git a/services/dis-apim-operator/internal/controller/backend_controller_test.go b/services/dis-apim-operator/internal/controller/backend_controller_test.go new file mode 100644 index 00000000..eaf682f8 --- /dev/null +++ b/services/dis-apim-operator/internal/controller/backend_controller_test.go @@ -0,0 +1,193 @@ +/* +Copyright 2024 altinn. + +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 controller + +import ( + "context" + "github.com/Altinn/altinn-platform/services/dis-apim-operator/internal/azure" + "github.com/Altinn/altinn-platform/services/dis-apim-operator/internal/config" + "github.com/Altinn/altinn-platform/services/dis-apim-operator/internal/utils" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" + azfake "github.com/Azure/azure-sdk-for-go/sdk/azcore/fake" + apim "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/apimanagement/armapimanagement/v2" + apimfake "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/apimanagement/armapimanagement/v2/fake" + "net/http" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + apimv1alpha1 "github.com/Altinn/altinn-platform/services/dis-apim-operator/api/v1alpha1" +) + +var _ = Describe("Backend Controller", func() { + Context("When reconciling a resource", func() { + const resourceName = "test-resource" + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", // TODO(user):Modify as needed + } + backend := &apimv1alpha1.Backend{} + + BeforeEach(func() { + By("creating the custom resource for the Kind Backend") + err := k8sClient.Get(ctx, typeNamespacedName, backend) + if err != nil && errors.IsNotFound(err) { + resource := &apimv1alpha1.Backend{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + Spec: apimv1alpha1.BackendSpec{ + Title: "test-backend", + Description: utils.ToPointer("Test backend for the operator"), + Url: "https://test.example.com", + }, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + + AfterEach(func() { + // TODO(user): Cleanup logic after each test, like removing the resource instance. + resource := &apimv1alpha1.Backend{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance Backend") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + It("should successfully reconcile the resource", func() { + fakeServer := &apimfake.BackendServer{ + CreateOrUpdate: func( + ctx context.Context, + resourceGroupName string, + serviceName string, + backendID string, + parameters apim.BackendContract, + options *apim.BackendClientCreateOrUpdateOptions, + ) (resp azfake.Responder[apim.BackendClientCreateOrUpdateResponse], errResp azfake.ErrorResponder) { + + response := apim.BackendClientCreateOrUpdateResponse{ + BackendContract: apim.BackendContract{ + Properties: parameters.Properties, + ID: utils.ToPointer("/subscriptions/fake-subscription/resourceGroups/fake-resource-group/providers/APIM/Backend/fake-apim-backend"), + Name: utils.ToPointer("fake-apim-backend"), + Type: utils.ToPointer("Microsoft.ApiManagement/service/backends"), + }, + } + + responder := azfake.Responder[apim.BackendClientCreateOrUpdateResponse]{} + + responder.SetResponse(http.StatusOK, response, nil) + + return responder, azfake.ErrorResponder{} + }, + Delete: func( + ctx context.Context, + resourceGroupName string, + serviceName string, + backendID string, + ifMatch string, + options *apim.BackendClientDeleteOptions, + ) (resp azfake.Responder[apim.BackendClientDeleteResponse], errResp azfake.ErrorResponder) { + response := apim.BackendClientDeleteResponse{} + responder := azfake.Responder[apim.BackendClientDeleteResponse]{} + + responder.SetResponse(http.StatusOK, response, nil) + + return responder, azfake.ErrorResponder{} + }, + Get: func( + ctx context.Context, + resourceGroupName string, + serviceName string, + backendID string, + options *apim.BackendClientGetOptions, + ) (resp azfake.Responder[apim.BackendClientGetResponse], errResp azfake.ErrorResponder) { + response := apim.BackendClientGetResponse{ + BackendContract: apim.BackendContract{ + Properties: &apim.BackendContractProperties{ + Protocol: utils.ToPointer(apim.BackendProtocolHTTP), + URL: utils.ToPointer("https://test.example.com"), + Description: utils.ToPointer("Test backend for the operator"), + TLS: &apim.BackendTLSProperties{ + ValidateCertificateChain: utils.ToPointer(true), + ValidateCertificateName: utils.ToPointer(true), + }, + Title: utils.ToPointer("test-backend"), + }, + }, + ETag: utils.ToPointer("33a64df551425fcc55e4d42a148795d9f25f89d5"), + } + responder := azfake.Responder[apim.BackendClientGetResponse]{} + + responder.SetResponse(http.StatusOK, response, nil) + errResponder := azfake.ErrorResponder{} + errResponder.SetResponseError(http.StatusNotFound, "Not Found") + return responder, errResponder + }, + GetEntityTag: nil, + NewListByServicePager: nil, + Reconnect: nil, + Update: nil, + } + transport := apimfake.NewBackendServerTransport(fakeServer) + factoryClientOptions := &arm.ClientOptions{ + ClientOptions: azcore.ClientOptions{ + Transport: transport, + }, + } + By("Reconciling the created resource") + controllerReconciler := &BackendReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + NewClient: azure.NewAPIMClient, + ApimClientConfig: &azure.ApimClientConfig{ + AzureConfig: config.AzureConfig{ + SubscriptionId: "fake-subscription-id", + ResourceGroup: "fake-resource-group", + ApimServiceName: "fake-apim-service", + }, + FactoryOptions: factoryClientOptions, + }, + } + + rsp, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(rsp).To(Equal(reconcile.Result{RequeueAfter: 1 * time.Minute})) + // Fetch the updated Backend resource + updatedBackend := &apimv1alpha1.Backend{} + err = k8sClient.Get(ctx, typeNamespacedName, updatedBackend) + Expect(err).NotTo(HaveOccurred()) + Expect(updatedBackend.Status.ProvisioningState).To(Equal(apimv1alpha1.BackendProvisioningStateSucceeded)) + // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. + // Example: If you expect a certain status condition after reconciliation, verify it here. + }) + }) +}) diff --git a/services/dis-apim-operator/internal/controller/suite_test.go b/services/dis-apim-operator/internal/controller/suite_test.go new file mode 100644 index 00000000..4b4aa822 --- /dev/null +++ b/services/dis-apim-operator/internal/controller/suite_test.go @@ -0,0 +1,96 @@ +/* +Copyright 2024 altinn. + +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 controller + +import ( + "context" + "fmt" + "path/filepath" + "runtime" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + apimv1alpha1 "github.com/Altinn/altinn-platform/services/dis-apim-operator/api/v1alpha1" + // +kubebuilder:scaffold:imports +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment +var ctx context.Context +var cancel context.CancelFunc + +func TestControllers(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Controller Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + ctx, cancel = context.WithCancel(context.TODO()) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + + // The BinaryAssetsDirectory is only required if you want to run the tests directly + // without call the makefile target test. If not informed it will look for the + // default path defined in controller-runtime which is /usr/local/kubebuilder/. + // Note that you must have the required binaries setup under the bin directory to perform + // the tests directly. When we run make test it will be setup and used automatically. + BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s", + fmt.Sprintf("1.31.0-%s-%s", runtime.GOOS, runtime.GOARCH)), + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + err = apimv1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + // +kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + cancel() + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) diff --git a/services/dis-apim-operator/internal/utils/types.go b/services/dis-apim-operator/internal/utils/types.go new file mode 100644 index 00000000..67587a57 --- /dev/null +++ b/services/dis-apim-operator/internal/utils/types.go @@ -0,0 +1,6 @@ +package utils + +// ToPointer gets the pointer of a value. +func ToPointer[T any](t T) *T { + return &t +} diff --git a/services/dis-apim-operator/internal/webhook/v1alpha1/backend_webhook.go b/services/dis-apim-operator/internal/webhook/v1alpha1/backend_webhook.go new file mode 100644 index 00000000..f8a63dfd --- /dev/null +++ b/services/dis-apim-operator/internal/webhook/v1alpha1/backend_webhook.go @@ -0,0 +1,84 @@ +/* +Copyright 2024 altinn. + +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 v1alpha1 + +import ( + "context" + "crypto/rand" + "encoding/hex" + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + + apimv1alpha1 "github.com/Altinn/altinn-platform/services/dis-apim-operator/api/v1alpha1" +) + +// nolint:unused +// log is for logging in this package. +var backendlog = logf.Log.WithName("backend-resource") + +// SetupBackendWebhookWithManager registers the webhook for Backend in the manager. +func SetupBackendWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr).For(&apimv1alpha1.Backend{}). + WithDefaulter(&BackendCustomDefaulter{}). + Complete() +} + +// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +// +kubebuilder:webhook:path=/mutate-apim-dis-altinn-cloud-v1alpha1-backend,mutating=true,failurePolicy=fail,sideEffects=None,groups=apim.dis.altinn.cloud,resources=backends,verbs=create;update,versions=v1alpha1,name=mbackend-v1alpha1.kb.io,admissionReviewVersions=v1 + +// BackendCustomDefaulter struct is responsible for setting default values on the custom resource of the +// Kind Backend when those are created or updated. +// +// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods, +// as it is used only for temporary operations and does not need to be deeply copied. +type BackendCustomDefaulter struct { + // TODO(user): Add more fields as needed for defaulting +} + +var _ webhook.CustomDefaulter = &BackendCustomDefaulter{} + +// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind Backend. +func (d *BackendCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error { + backend, ok := obj.(*apimv1alpha1.Backend) + + if !ok { + return fmt.Errorf("expected an Backend object but got %T", obj) + } + backendlog.Info("Defaulting for Backend", "name", backend.GetName()) + if backend.Spec.AzureResourcePrefix == nil { + randomString, err := generateRandomString(8) + if err != nil { + return err + } + backend.Spec.AzureResourcePrefix = &randomString + } + return nil +} + +// generateRandomString generates a random string of the given length +func generateRandomString(length int) (string, error) { + bytes := make([]byte, length) + if _, err := rand.Read(bytes); err != nil { + return "", err + } + return hex.EncodeToString(bytes)[:length], nil +} diff --git a/services/dis-apim-operator/internal/webhook/v1alpha1/backend_webhook_test.go b/services/dis-apim-operator/internal/webhook/v1alpha1/backend_webhook_test.go new file mode 100644 index 00000000..6644d492 --- /dev/null +++ b/services/dis-apim-operator/internal/webhook/v1alpha1/backend_webhook_test.go @@ -0,0 +1,67 @@ +/* +Copyright 2024 altinn. + +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 v1alpha1 + +import ( + "github.com/Altinn/altinn-platform/services/dis-apim-operator/internal/utils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + apimv1alpha1 "github.com/Altinn/altinn-platform/services/dis-apim-operator/api/v1alpha1" + // TODO (user): Add any additional imports if needed +) + +var _ = Describe("Backend Webhook", func() { + var ( + obj *apimv1alpha1.Backend + oldObj *apimv1alpha1.Backend + defaulter BackendCustomDefaulter + ) + + BeforeEach(func() { + obj = &apimv1alpha1.Backend{} + oldObj = &apimv1alpha1.Backend{} + defaulter = BackendCustomDefaulter{} + Expect(defaulter).NotTo(BeNil(), "Expected defaulter to be initialized") + Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") + Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") + }) + + AfterEach(func() { + }) + + Context("When creating Backend under Defaulting Webhook", func() { + It("Should add a random string to AzureResourcePrefix if not provided", func() { + By("simulating a scenario where AzureResourcePrefix is not provided") + obj.Spec.AzureResourcePrefix = nil + By("calling the Default method to apply defaults") + defaulter.Default(ctx, obj) + By("checking that a random string is added to AzureResourcePrefix") + Expect(obj.Spec.AzureResourcePrefix).NotTo(BeNil()) + Expect(*obj.Spec.AzureResourcePrefix).To(HaveLen(8)) + }) + It("Should not add a random string to AzureResourcePrefix if provided", func() { + By("simulating a scenario where AzureResourcePrefix is provided") + obj.Spec.AzureResourcePrefix = utils.ToPointer("test") + By("calling the Default method to apply defaults") + defaulter.Default(ctx, obj) + By("checking that AzureResourcePrefix is not changed") + Expect(*obj.Spec.AzureResourcePrefix).To(Equal("test")) + }) + }) + +}) diff --git a/services/dis-apim-operator/internal/webhook/v1alpha1/webhook_suite_test.go b/services/dis-apim-operator/internal/webhook/v1alpha1/webhook_suite_test.go new file mode 100644 index 00000000..7a516331 --- /dev/null +++ b/services/dis-apim-operator/internal/webhook/v1alpha1/webhook_suite_test.go @@ -0,0 +1,150 @@ +/* +Copyright 2024 altinn. + +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 v1alpha1 + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "path/filepath" + "runtime" + "testing" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + admissionv1 "k8s.io/api/admission/v1" + + apimv1alpha1 "github.com/Altinn/altinn-platform/services/dis-apim-operator/api/v1alpha1" + + // +kubebuilder:scaffold:imports + apimachineryruntime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/webhook" +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var ( + cancel context.CancelFunc + cfg *rest.Config + ctx context.Context + k8sClient client.Client + testEnv *envtest.Environment +) + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Webhook Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + ctx, cancel = context.WithCancel(context.TODO()) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: false, + + // The BinaryAssetsDirectory is only required if you want to run the tests directly + // without call the makefile target test. If not informed it will look for the + // default path defined in controller-runtime which is /usr/local/kubebuilder/. + // Note that you must have the required binaries setup under the bin directory to perform + // the tests directly. When we run make test it will be setup and used automatically. + BinaryAssetsDirectory: filepath.Join("..", "..", "..", "bin", "k8s", + fmt.Sprintf("1.31.0-%s-%s", runtime.GOOS, runtime.GOARCH)), + + WebhookInstallOptions: envtest.WebhookInstallOptions{ + Paths: []string{filepath.Join("..", "..", "..", "config", "webhook")}, + }, + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + scheme := apimachineryruntime.NewScheme() + err = apimv1alpha1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + err = admissionv1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + // +kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + + // start webhook server using Manager. + webhookInstallOptions := &testEnv.WebhookInstallOptions + mgr, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme, + WebhookServer: webhook.NewServer(webhook.Options{ + Host: webhookInstallOptions.LocalServingHost, + Port: webhookInstallOptions.LocalServingPort, + CertDir: webhookInstallOptions.LocalServingCertDir, + }), + LeaderElection: false, + Metrics: metricsserver.Options{BindAddress: "0"}, + }) + Expect(err).NotTo(HaveOccurred()) + + err = SetupBackendWebhookWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + + // +kubebuilder:scaffold:webhook + + go func() { + defer GinkgoRecover() + err = mgr.Start(ctx) + Expect(err).NotTo(HaveOccurred()) + }() + + // wait for the webhook server to get ready. + dialer := &net.Dialer{Timeout: time.Second} + addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort) + Eventually(func() error { + conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true}) + if err != nil { + return err + } + + return conn.Close() + }).Should(Succeed()) +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + cancel() + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) diff --git a/services/dis-apim-operator/test/e2e/e2e_test.go b/services/dis-apim-operator/test/e2e/e2e_test.go index d7c491d6..62bee7cc 100644 --- a/services/dis-apim-operator/test/e2e/e2e_test.go +++ b/services/dis-apim-operator/test/e2e/e2e_test.go @@ -234,6 +234,30 @@ var _ = Describe("Manager", Ordered, func() { )) }) + It("should provisioned cert-manager", func() { + By("validating that cert-manager has the certificate Secret") + verifyCertManager := func(g Gomega) { + cmd := exec.Command("kubectl", "get", "secrets", "webhook-server-cert", "-n", namespace) + _, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + } + Eventually(verifyCertManager).Should(Succeed()) + }) + + It("should have CA injection for mutating webhooks", func() { + By("checking CA injection for mutating webhooks") + verifyCAInjection := func(g Gomega) { + cmd := exec.Command("kubectl", "get", + "mutatingwebhookconfigurations.admissionregistration.k8s.io", + "dis-apim-operator-mutating-webhook-configuration", + "-o", "go-template={{ range .webhooks }}{{ .clientConfig.caBundle }}{{ end }}") + mwhOutput, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(len(mwhOutput)).To(BeNumerically(">", 10)) + } + Eventually(verifyCAInjection).Should(Succeed()) + }) + // +kubebuilder:scaffold:e2e-webhooks-checks // TODO: Customize the e2e test suite with scenarios specific to your project.