diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..7457664 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,16 @@ +--- +name: Bug Report +about: Report a bug +labels: kind/bug + +--- + +**What happened**: + +**What you expected to happen**: + +**How to reproduce it (as minimally and precisely as possible)**: + +**Anything else we need to know**: + +**Environment**: diff --git a/.github/ISSUE_TEMPLATE/enhancement_request.md b/.github/ISSUE_TEMPLATE/enhancement_request.md new file mode 100644 index 0000000..4179e17 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/enhancement_request.md @@ -0,0 +1,10 @@ +--- +name: Enhancement Request +about: Suggest an enhancement +labels: kind/enhancement + +--- + +**What would you like to be added**: + +**Why is this needed**: diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..1fe33e5 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,20 @@ +**What this PR does / why we need it**: + +**Which issue(s) this PR fixes**: +Fixes # + +**Special notes for your reviewer**: + +**Release note**: + +```feature user + +``` diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..cb8b3d6 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,18 @@ +name: CI + +on: + push: + tags: + - v* + branches: + - master + - main + pull_request: + +permissions: + contents: read + +jobs: + build_validate_test: + uses: openmcp-project/build/.github/workflows/ci.lib.yaml@main + secrets: inherit diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..fdcd53c --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,15 @@ +name: Publish + +on: + push: + tags: + - v* + workflow_dispatch: + +permissions: + packages: write + +jobs: + release_publish: + uses: openmcp-project/build/.github/workflows/publish.lib.yaml@main + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..bbdc43f --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,15 @@ +name: Versioned Release + +on: + push: + branches: + - main + +permissions: + contents: write # we need this to be able to push tags + pull-requests: read + +jobs: + release_tag: + uses: openmcp-project/build/.github/workflows/release.lib.yaml@main + secrets: inherit diff --git a/.github/workflows/reuse.yaml b/.github/workflows/reuse.yaml new file mode 100644 index 0000000..aa0ba49 --- /dev/null +++ b/.github/workflows/reuse.yaml @@ -0,0 +1,11 @@ +name: REUSE Compliance Check + +on: [push, pull_request] + +permissions: + contents: read + +jobs: + run_reuse: + uses: openmcp-project/build/.github/workflows/reuse.lib.yaml@main + secrets: inherit diff --git a/.github/workflows/validate-pr-content.yaml b/.github/workflows/validate-pr-content.yaml new file mode 100644 index 0000000..52a07c0 --- /dev/null +++ b/.github/workflows/validate-pr-content.yaml @@ -0,0 +1,15 @@ +name: Validate Pull Request Content + +on: + pull_request: + types: + - opened + - edited + +permissions: + contents: read + +jobs: + validate_pr_content: + uses: openmcp-project/build/.github/workflows/validate-pr-content.lib.yaml@main + secrets: inherit \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1406b72 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ + + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin/ +Dockerfile.cross + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Kubernetes Generated files - skip generated files, except for vendored files + +!vendor/**/zz_generated.* + +# editor and IDE paraphernalia +.idea +.vscode +*.swp +*.swo +*~ + +tmp/ +go.work* +components/ +!**/components/ +/**/cover.html +/**/cover.*.html + +*.tmp +.task/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..c0425ec --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "hack/common"] + path = hack/common + url = https://github.com/openmcp-project/build diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..9a69a7f --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,46 @@ +version: "2" +run: + allow-parallel-runners: true +linters: + default: none + enable: + - copyloopvar + - dupl + - errcheck + - ginkgolinter + - goconst + - gocyclo + - govet + - ineffassign + - misspell + - nakedret + - prealloc + - revive + - staticcheck + - unconvert + - unparam + - unused + settings: + revive: + rules: + - name: comment-spacings + exclusions: + generated: lax + rules: + - linters: + - dupl + path: internal/* + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gofmt + - goimports + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 204321b..c2d6bf6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ ## Code of Conduct -All members of the project community must abide by the [SAP Open Source Code of Conduct](https://github.com/SAP/.github/blob/main/CODE_OF_CONDUCT.md). +All members of the project community must abide by the [SAP Open Source Code of Conduct](https://github.com/openmcp-project/.github/blob/main/CODE_OF_CONDUCT.md). Only by respecting each other we can develop a productive, collaborative community. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [ospo@sap.com](mailto:ospo@sap.com) (SAP Open Source Program Office). All complaints will be reviewed and investigated promptly and fairly. @@ -30,7 +30,7 @@ The following rule governs code contributions: * Contributions must be licensed under the [Apache 2.0 License](./LICENSE). * Due to legal reasons, contributors will be asked to accept a Developer Certificate of Origin (DCO) when they create the first pull request to this project. This happens in an automated fashion during the submission process. SAP uses [the standard DCO text of the Linux Foundation](https://developercertificate.org/). -* Contributions must follow our [guidelines on AI-generated code](https://github.com/SAP/.github/blob/main/CONTRIBUTING_USING_GENAI.md) in case you are using such tools. +* Contributions must follow our [guidelines on AI-generated code](https://github.com/openmcp-project/.github/blob/main/CONTRIBUTING_USING_GENAI.md) in case you are using such tools. ## Issues and Planning diff --git a/Makefile b/Makefile new file mode 120000 index 0000000..c21893a --- /dev/null +++ b/Makefile @@ -0,0 +1 @@ +./hack/common/Makefile \ No newline at end of file diff --git a/REUSE.toml b/REUSE.toml index 9826e3e..aafab8e 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -1,6 +1,6 @@ version = 1 SPDX-PackageName = "platform-service-dns" -SPDX-PackageDownloadLocation = "" +SPDX-PackageDownloadLocation = "https://github.com/openmcp-project/platform-service-dns" SPDX-PackageComment = "The code in this project may include calls to APIs (\"API Calls\") of\n SAP or third-party products or services developed outside of this project\n (\"External Products\").\n \"APIs\" means application programming interfaces, as well as their respective\n specifications and implementing code that allows software to communicate with\n other software.\n API Calls to External Products are not licensed under the open source license\n that governs this project. The use of such API Calls and related External\n Products are subject to applicable additional agreements with the relevant\n provider of the External Products. In no event shall the open source license\n that governs this project grant any rights in or to any External Products, or\n alter, expand or supersede any terms of the applicable additional agreements.\n If you have a valid license agreement with SAP for the use of a particular SAP\n External Product, then you may make use of any API Calls included in this\n project's code for that SAP External Product, subject to the terms of such\n license agreement. If you do not have a valid license agreement for the use of\n a particular SAP External Product, then you may only make use of any API Calls\n in this project for that SAP External Product for your internal, non-productive\n and non-commercial test and evaluation of such API Calls. Nothing herein grants\n you any rights to use or access any SAP External Product, or provide any third\n parties the right to use of access any SAP External Product, through API Calls." [[annotations]] diff --git a/Taskfile.yaml b/Taskfile.yaml new file mode 100644 index 0000000..6916a2f --- /dev/null +++ b/Taskfile.yaml @@ -0,0 +1,18 @@ +version: 3 + +includes: + shared: + taskfile: hack/common/Taskfile_controller.yaml + flatten: true + excludes: [] # put task names in here which are overwritten in this file + vars: + NESTED_MODULES: api + API_DIRS: '{{.ROOT_DIR}}/api/...' + MANIFEST_OUT: '{{.ROOT_DIR}}/api/crds/manifests' + CODE_DIRS: '{{.ROOT_DIR}}/cmd/... {{.ROOT_DIR}}/internal/... {{.ROOT_DIR}}/api/...' + COMPONENTS: platform-service-dns + REPO_URL: 'https://github.com/openmcp-project/platform-service-dns' + GENERATE_DOCS_INDEX: "false" + CHART_COMPONENTS: "[]" + CRDS_COMPONENTS: platform-service-dns + CRDS_PATH: '{{.ROOT_DIR}}/api/crds/manifests' diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..e7685f2 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +v0.0.0-dev \ No newline at end of file diff --git a/api/crds/manifests/dns.openmcp.cloud_dnsserviceconfigs.yaml b/api/crds/manifests/dns.openmcp.cloud_dnsserviceconfigs.yaml new file mode 100644 index 0000000..46cd644 --- /dev/null +++ b/api/crds/manifests/dns.openmcp.cloud_dnsserviceconfigs.yaml @@ -0,0 +1,709 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + labels: + openmcp.cloud/cluster: platform + name: dnsserviceconfigs.dns.openmcp.cloud +spec: + group: dns.openmcp.cloud + names: + kind: DNSServiceConfig + listKind: DNSServiceConfigList + plural: dnsserviceconfigs + shortNames: + - dnscfg + singular: dnsserviceconfig + scope: Cluster + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: DNSServiceConfig is the Schema for the DNS PlatformService configuration + 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: DNSServiceConfigSpec defines the desired state of DNSServiceConfig + properties: + externalDNSForPurposes: + description: |- + ExternalDNSForPurposes is a list of DNS configurations in combination with purpose selectors. + The first matching purpose selector will be applied to the Cluster. + If no selector matches, no configuration will be applied. + items: + description: ExternalDNSPurposeConfig holds a purpose selector and + the DNS configuration to apply if the selector matches. + properties: + config: + description: HelmValues are the helm values to deploy external-dns + with, if the purpose selector matches. + name: + description: |- + Name is an optional name. + It can be set to more easily identify the configuration in logs and events. + type: string + purposeSelector: + description: |- + PurposeSelector is a selector to match against the list of purposes of a Cluster. + If not set, all Clusters are matched. + properties: + and: + items: {} + type: array + name: + type: string + not: {} + or: + items: {} + type: array + type: object + x-kubernetes-validations: + - message: Exactly one of 'and', 'or', 'not' or 'name' must + be set + rule: size(self.filter(property, size(self[property]) > 0)) + == 1 + required: + - config + type: object + type: array + externalDNSSource: + description: ExternalDNSSource is the source of the external-dns helm + chart. + properties: + copyAuthSecret: + description: |- + SecretCopy defines the name of the secret to copy and the name of the copied secret. + If target is nil or target.name is empty, the secret will be copied with the same name as the source secret. + properties: + source: + description: ObjectReference is a reference to an object in + any namespace. + properties: + name: + description: Name is the name of the object. + type: string + namespace: + description: Namespace is the namespace of the object. + type: string + required: + - name + - namespace + type: object + target: + description: ObjectReference is a reference to an object in + any namespace. + properties: + name: + description: Name is the name of the object. + type: string + namespace: + description: Namespace is the namespace of the object. + type: string + required: + - name + - namespace + type: object + required: + - source + - target + type: object + git: + description: |- + GitRepositorySpec specifies the required configuration to produce an + Artifact for a Git repository. + properties: + ignore: + description: |- + Ignore overrides the set of excluded patterns in the .sourceignore format + (which is the same as .gitignore). If not provided, a default will be used, + consult the documentation for your version to find out what those are. + type: string + include: + description: |- + Include specifies a list of GitRepository resources which Artifacts + should be included in the Artifact produced for this GitRepository. + items: + description: |- + GitRepositoryInclude specifies a local reference to a GitRepository which + Artifact (sub-)contents must be included, and where they should be placed. + properties: + fromPath: + description: |- + FromPath specifies the path to copy contents from, defaults to the root + of the Artifact. + type: string + repository: + description: |- + GitRepositoryRef specifies the GitRepository which Artifact contents + must be included. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + toPath: + description: |- + ToPath specifies the path to copy contents to, defaults to the name of + the GitRepositoryRef. + type: string + required: + - repository + type: object + type: array + interval: + description: |- + Interval at which the GitRepository URL is checked for updates. + This interval is approximate and may be subject to jitter to ensure + efficient use of resources. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ + type: string + provider: + description: |- + Provider used for authentication, can be 'azure', 'github', 'generic'. + When not specified, defaults to 'generic'. + enum: + - generic + - azure + - github + type: string + proxySecretRef: + description: |- + ProxySecretRef specifies the Secret containing the proxy configuration + to use while communicating with the Git server. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + recurseSubmodules: + description: |- + RecurseSubmodules enables the initialization of all submodules within + the GitRepository as cloned from the URL, using their default settings. + type: boolean + ref: + description: |- + Reference specifies the Git reference to resolve and monitor for + changes, defaults to the 'master' branch. + properties: + branch: + description: Branch to check out, defaults to 'master' + if no other field is defined. + type: string + commit: + description: |- + Commit SHA to check out, takes precedence over all reference fields. + + This can be combined with Branch to shallow clone the branch, in which + the commit is expected to exist. + type: string + name: + description: |- + Name of the reference to check out; takes precedence over Branch, Tag and SemVer. + + It must be a valid Git reference: https://git-scm.com/docs/git-check-ref-format#_description + Examples: "refs/heads/main", "refs/tags/v0.1.0", "refs/pull/420/head", "refs/merge-requests/1/head" + type: string + semver: + description: SemVer tag expression to check out, takes + precedence over Tag. + type: string + tag: + description: Tag to check out, takes precedence over Branch. + type: string + type: object + secretRef: + description: |- + SecretRef specifies the Secret containing authentication credentials for + the GitRepository. + For HTTPS repositories the Secret must contain 'username' and 'password' + fields for basic auth or 'bearerToken' field for token auth. + For SSH repositories the Secret must contain 'identity' + and 'known_hosts' fields. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + sparseCheckout: + description: |- + SparseCheckout specifies a list of directories to checkout when cloning + the repository. If specified, only these directories are included in the + Artifact produced for this GitRepository. + items: + type: string + type: array + suspend: + description: |- + Suspend tells the controller to suspend the reconciliation of this + GitRepository. + type: boolean + timeout: + default: 60s + description: Timeout for Git operations like cloning, defaults + to 60s. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m))+$ + type: string + url: + description: URL specifies the Git repository URL, it can + be an HTTP/S or SSH address. + pattern: ^(http|https|ssh)://.*$ + type: string + verify: + description: |- + Verification specifies the configuration to verify the Git commit + signature(s). + properties: + mode: + default: HEAD + description: |- + Mode specifies which Git object(s) should be verified. + + The variants "head" and "HEAD" both imply the same thing, i.e. verify + the commit that the HEAD of the Git repository points to. The variant + "head" solely exists to ensure backwards compatibility. + enum: + - head + - HEAD + - Tag + - TagAndHEAD + type: string + secretRef: + description: |- + SecretRef specifies the Secret containing the public keys of trusted Git + authors. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + required: + - secretRef + type: object + required: + - interval + - url + type: object + helm: + description: |- + HelmRepositorySpec specifies the required configuration to produce an + Artifact for a Helm repository index YAML. + properties: + accessFrom: + description: |- + AccessFrom specifies an Access Control List for allowing cross-namespace + references to this object. + NOTE: Not implemented, provisional as of https://github.com/fluxcd/flux2/pull/2092 + properties: + namespaceSelectors: + description: |- + NamespaceSelectors is the list of namespace selectors to which this ACL applies. + Items in this list are evaluated using a logical OR operation. + items: + description: |- + NamespaceSelector selects the namespaces to which this ACL applies. + An empty map of MatchLabels matches all namespaces in a cluster. + properties: + matchLabels: + additionalProperties: + type: string + description: |- + MatchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + type: array + required: + - namespaceSelectors + type: object + certSecretRef: + description: |- + CertSecretRef can be given the name of a Secret containing + either or both of + + - a PEM-encoded client certificate (`tls.crt`) and private + key (`tls.key`); + - a PEM-encoded CA certificate (`ca.crt`) + + and whichever are supplied, will be used for connecting to the + registry. The client cert and key are useful if you are + authenticating with a certificate; the CA cert is useful if + you are using a self-signed server certificate. The Secret must + be of type `Opaque` or `kubernetes.io/tls`. + + It takes precedence over the values specified in the Secret referred + to by `.spec.secretRef`. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + insecure: + description: |- + Insecure allows connecting to a non-TLS HTTP container registry. + This field is only taken into account if the .spec.type field is set to 'oci'. + type: boolean + interval: + description: |- + Interval at which the HelmRepository URL is checked for updates. + This interval is approximate and may be subject to jitter to ensure + efficient use of resources. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ + type: string + passCredentials: + description: |- + PassCredentials allows the credentials from the SecretRef to be passed + on to a host that does not match the host as defined in URL. + This may be required if the host of the advertised chart URLs in the + index differ from the defined URL. + Enabling this should be done with caution, as it can potentially result + in credentials getting stolen in a MITM-attack. + type: boolean + provider: + default: generic + description: |- + Provider used for authentication, can be 'aws', 'azure', 'gcp' or 'generic'. + This field is optional, and only taken into account if the .spec.type field is set to 'oci'. + When not specified, defaults to 'generic'. + enum: + - generic + - aws + - azure + - gcp + type: string + secretRef: + description: |- + SecretRef specifies the Secret containing authentication credentials + for the HelmRepository. + For HTTP/S basic auth the secret must contain 'username' and 'password' + fields. + Support for TLS auth using the 'certFile' and 'keyFile', and/or 'caFile' + keys is deprecated. Please use `.spec.certSecretRef` instead. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + suspend: + description: |- + Suspend tells the controller to suspend the reconciliation of this + HelmRepository. + type: boolean + timeout: + description: |- + Timeout is used for the index fetch operation for an HTTPS helm repository, + and for remote OCI Repository operations like pulling for an OCI helm + chart by the associated HelmChart. + Its default value is 60s. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m))+$ + type: string + type: + description: |- + Type of the HelmRepository. + When this field is set to "oci", the URL field value must be prefixed with "oci://". + enum: + - default + - oci + type: string + url: + description: |- + URL of the Helm repository, a valid URL contains at least a protocol and + host. + pattern: ^(http|https|oci)://.*$ + type: string + required: + - url + type: object + oci: + description: OCIRepositorySpec defines the desired state of OCIRepository + properties: + certSecretRef: + description: |- + CertSecretRef can be given the name of a Secret containing + either or both of + + - a PEM-encoded client certificate (`tls.crt`) and private + key (`tls.key`); + - a PEM-encoded CA certificate (`ca.crt`) + + and whichever are supplied, will be used for connecting to the + registry. The client cert and key are useful if you are + authenticating with a certificate; the CA cert is useful if + you are using a self-signed server certificate. The Secret must + be of type `Opaque` or `kubernetes.io/tls`. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + ignore: + description: |- + Ignore overrides the set of excluded patterns in the .sourceignore format + (which is the same as .gitignore). If not provided, a default will be used, + consult the documentation for your version to find out what those are. + type: string + insecure: + description: Insecure allows connecting to a non-TLS HTTP + container registry. + type: boolean + interval: + description: |- + Interval at which the OCIRepository URL is checked for updates. + This interval is approximate and may be subject to jitter to ensure + efficient use of resources. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ + type: string + layerSelector: + description: |- + LayerSelector specifies which layer should be extracted from the OCI artifact. + When not specified, the first layer found in the artifact is selected. + properties: + mediaType: + description: |- + MediaType specifies the OCI media type of the layer + which should be extracted from the OCI Artifact. The + first layer matching this type is selected. + type: string + operation: + description: |- + Operation specifies how the selected layer should be processed. + By default, the layer compressed content is extracted to storage. + When the operation is set to 'copy', the layer compressed content + is persisted to storage as it is. + enum: + - extract + - copy + type: string + type: object + provider: + default: generic + description: |- + The provider used for authentication, can be 'aws', 'azure', 'gcp' or 'generic'. + When not specified, defaults to 'generic'. + enum: + - generic + - aws + - azure + - gcp + type: string + proxySecretRef: + description: |- + ProxySecretRef specifies the Secret containing the proxy configuration + to use while communicating with the container registry. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + ref: + description: |- + The OCI reference to pull and monitor for changes, + defaults to the latest tag. + properties: + digest: + description: |- + Digest is the image digest to pull, takes precedence over SemVer. + The value should be in the format 'sha256:'. + type: string + semver: + description: |- + SemVer is the range of tags to pull selecting the latest within + the range, takes precedence over Tag. + type: string + semverFilter: + description: SemverFilter is a regex pattern to filter + the tags within the SemVer range. + type: string + tag: + description: Tag is the image tag to pull, defaults to + latest. + type: string + type: object + secretRef: + description: |- + SecretRef contains the secret name containing the registry login + credentials to resolve image metadata. + The secret must be of type kubernetes.io/dockerconfigjson. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + serviceAccountName: + description: |- + ServiceAccountName is the name of the Kubernetes ServiceAccount used to authenticate + the image pull if the service account has attached pull secrets. For more information: + https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#add-imagepullsecrets-to-a-service-account + type: string + suspend: + description: This flag tells the controller to suspend the + reconciliation of this source. + type: boolean + timeout: + default: 60s + description: The timeout for remote OCI Repository operations + like pulling, defaults to 60s. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m))+$ + type: string + url: + description: |- + URL is a reference to an OCI artifact repository hosted + on a remote container registry. + pattern: ^oci://.*$ + type: string + verify: + description: |- + Verify contains the secret name containing the trusted public keys + used to verify the signature and specifies which provider to use to check + whether OCI image is authentic. + properties: + matchOIDCIdentity: + description: |- + MatchOIDCIdentity specifies the identity matching criteria to use + while verifying an OCI artifact which was signed using Cosign keyless + signing. The artifact's identity is deemed to be verified if any of the + specified matchers match against the identity. + items: + description: |- + OIDCIdentityMatch specifies options for verifying the certificate identity, + i.e. the issuer and the subject of the certificate. + properties: + issuer: + description: |- + Issuer specifies the regex pattern to match against to verify + the OIDC issuer in the Fulcio certificate. The pattern must be a + valid Go regular expression. + type: string + subject: + description: |- + Subject specifies the regex pattern to match against to verify + the identity subject in the Fulcio certificate. The pattern must + be a valid Go regular expression. + type: string + required: + - issuer + - subject + type: object + type: array + provider: + default: cosign + description: Provider specifies the technology used to + sign the OCI Artifact. + enum: + - cosign + - notation + type: string + secretRef: + description: |- + SecretRef specifies the Kubernetes Secret containing the + trusted public keys. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + required: + - provider + type: object + required: + - interval + - url + type: object + type: object + x-kubernetes-validations: + - message: Exactly one of 'helm', 'git', or 'oci' must be set + rule: size(self.filter(property, (property != "copyAuthSecret") + && (size(self[property]) > 0))) == 1 + selector: + description: |- + Selector is a label selector. + If not nil, only Clusters that match the selector will be reconciled by the controller. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - externalDNSSource + type: object + type: object + served: true + storage: true diff --git a/api/dns/v1alpha1/config_types.go b/api/dns/v1alpha1/config_types.go new file mode 100644 index 0000000..abc5433 --- /dev/null +++ b/api/dns/v1alpha1/config_types.go @@ -0,0 +1,149 @@ +package v1alpha1 + +import ( + "slices" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + + fluxv1 "github.com/fluxcd/source-controller/api/v1" + + commonapi "github.com/openmcp-project/openmcp-operator/api/common" +) + +// DNSServiceConfigSpec defines the desired state of DNSServiceConfig +type DNSServiceConfigSpec struct { + // Selector is a label selector. + // If not nil, only Clusters that match the selector will be reconciled by the controller. + // +optional + Selector *metav1.LabelSelector `json:"selector,omitempty"` + + // ExternalDNSSource is the source of the external-dns helm chart. + ExternalDNSSource ExternalDNSSource `json:"externalDNSSource"` + + // ExternalDNSForPurposes is a list of DNS configurations in combination with purpose selectors. + // The first matching purpose selector will be applied to the Cluster. + // If no selector matches, no configuration will be applied. + // +optional + ExternalDNSForPurposes []ExternalDNSPurposeConfig `json:"externalDNSForPurposes,omitempty"` +} + +// ExternalDNSSource defines the source of the external-dns helm chart in form of a Flux source. +// Exactly one of 'HelmRepository', 'GitRepository' or 'OCIRepository' must be set. +// If 'copyAuthSecret' is set, the referenced source secret is copied into the namespace where the Flux resources are created with the specified target name. +// +kubebuilder:validation:XValidation:rule=`size(self.filter(property, (property != "copyAuthSecret") && (size(self[property]) > 0))) == 1`, message="Exactly one of 'helm', 'git', or 'oci' must be set" +type ExternalDNSSource struct { + Helm *fluxv1.HelmRepositorySpec `json:"helm,omitempty"` + Git *fluxv1.GitRepositorySpec `json:"git,omitempty"` + OCI *fluxv1.OCIRepositorySpec `json:"oci,omitempty"` + CopyAuthSecret *SecretCopy `json:"copyAuthSecret,omitempty"` +} + +// SecretCopy defines the name of the secret to copy and the name of the copied secret. +// If target is nil or target.name is empty, the secret will be copied with the same name as the source secret. +type SecretCopy struct { + Source commonapi.ObjectReference `json:"source"` + Target *commonapi.ObjectReference `json:"target"` +} + +// ExternalDNSPurposeConfig holds a purpose selector and the DNS configuration to apply if the selector matches. +type ExternalDNSPurposeConfig struct { + // Name is an optional name. + // It can be set to more easily identify the configuration in logs and events. + // +optional + Name string `json:"name,omitempty"` + // PurposeSelector is a selector to match against the list of purposes of a Cluster. + // If not set, all Clusters are matched. + // +optional + PurposeSelector *PurposeSelector `json:"purposeSelector,omitempty"` + // HelmValues are the helm values to deploy external-dns with, if the purpose selector matches. + // +kubebuilder:validation:Schemaless + HelmValues runtime.RawExtension `json:"config"` +} + +// PurposeSelector is a selector to match against the list of purposes of a Cluster. +type PurposeSelector struct { + PurposeSelectorRequirement `json:",inline"` +} + +// PurposeSelectorRequirement is a selector to select purposes to apply the configuration to. +// The struct can be combined recursively using "and", "or" and "not" to build complex selectors. +// Exactly one of the fields must be set. +// If name is set, the selector matches if the Cluster's purposes contain the given name. +// If and is set, the selector matches if all of the contained selectors match. +// If or is set, the selector matches if any of the contained selectors match. +// If not is set, the selector matches if the contained selector does not match. +// +kubebuilder:validation:XValidation:rule=`size(self.filter(property, size(self[property]) > 0)) == 1`, message="Exactly one of 'and', 'or', 'not' or 'name' must be set" +type PurposeSelectorRequirement struct { + And []PurposeSelectorRequirement `json:"and,omitempty"` + Or []PurposeSelectorRequirement `json:"or,omitempty"` + Not *PurposeSelectorRequirement `json:"not,omitempty"` + Name string `json:"name,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:metadata:labels="openmcp.cloud/cluster=platform" +// +kubebuilder:resource:scope=Cluster,shortName=dnscfg + +// DNSServiceConfig is the Schema for the DNS PlatformService configuration API +type DNSServiceConfig struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec DNSServiceConfigSpec `json:"spec,omitempty"` +} + +// +kubebuilder:object:root=true + +// DNSServiceConfigList contains a list of DNSServiceConfig +type DNSServiceConfigList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []DNSServiceConfig `json:"items"` +} + +func init() { + SchemeBuilder.Register(&DNSServiceConfig{}, &DNSServiceConfigList{}) +} + +// Matches returns true if the selector matches the given list of purposes. +func (ps *PurposeSelector) Matches(purposes []string) bool { + return requirementMatches(&ps.PurposeSelectorRequirement, purposes, map[*PurposeSelectorRequirement]empty{}) +} + +type empty struct{} + +func requirementMatches(r *PurposeSelectorRequirement, purposes []string, seenRequirements map[*PurposeSelectorRequirement]empty) bool { + if r == nil { + return true + } + if _, ok := seenRequirements[r]; ok { + panic("circular reference in PurposeSelectorRequirement") + } + seenRequirements[r] = empty{} + defer delete(seenRequirements, r) + + if r.Name != "" { + return slices.Contains(purposes, r.Name) + } + if len(r.And) > 0 { + for i := range r.And { + if !requirementMatches(&r.And[i], purposes, seenRequirements) { + return false + } + } + return true + } + if len(r.Or) > 0 { + for i := range r.Or { + if requirementMatches(&r.Or[i], purposes, seenRequirements) { + return true + } + } + return false + } + if r.Not != nil { + return !requirementMatches(r.Not, purposes, seenRequirements) + } + return false +} diff --git a/api/dns/v1alpha1/constants.go b/api/dns/v1alpha1/constants.go new file mode 100644 index 0000000..b9cae29 --- /dev/null +++ b/api/dns/v1alpha1/constants.go @@ -0,0 +1,11 @@ +package v1alpha1 + +import ( + openmcpconst "github.com/openmcp-project/openmcp-operator/api/constants" +) + +const ( + OperationAnnotation = "dns." + openmcpconst.OperationAnnotation + + ExternalDNSFinalizerOnCluster = "platformservice." + openmcpconst.OpenMCPGroupName + "/dns" +) diff --git a/api/dns/v1alpha1/groupversion_info.go b/api/dns/v1alpha1/groupversion_info.go new file mode 100644 index 0000000..d0d81df --- /dev/null +++ b/api/dns/v1alpha1/groupversion_info.go @@ -0,0 +1,23 @@ +// +kubebuilder:object:generate=true +// +groupName=dns.openmcp.cloud +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" + + apiconst "github.com/openmcp-project/openmcp-operator/api/constants" +) + +const GroupName = "dns." + apiconst.OpenMCPGroupName + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: GroupName, 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/api/dns/v1alpha1/zz_generated.deepcopy.go b/api/dns/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 0000000..21079ef --- /dev/null +++ b/api/dns/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,225 @@ +//go:build !ignore_autogenerated + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + apiv1 "github.com/fluxcd/source-controller/api/v1" + "github.com/openmcp-project/openmcp-operator/api/common" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSServiceConfig) DeepCopyInto(out *DNSServiceConfig) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSServiceConfig. +func (in *DNSServiceConfig) DeepCopy() *DNSServiceConfig { + if in == nil { + return nil + } + out := new(DNSServiceConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSServiceConfig) 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 *DNSServiceConfigList) DeepCopyInto(out *DNSServiceConfigList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DNSServiceConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSServiceConfigList. +func (in *DNSServiceConfigList) DeepCopy() *DNSServiceConfigList { + if in == nil { + return nil + } + out := new(DNSServiceConfigList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSServiceConfigList) 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 *DNSServiceConfigSpec) DeepCopyInto(out *DNSServiceConfigSpec) { + *out = *in + if in.Selector != nil { + in, out := &in.Selector, &out.Selector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } + in.ExternalDNSSource.DeepCopyInto(&out.ExternalDNSSource) + if in.ExternalDNSForPurposes != nil { + in, out := &in.ExternalDNSForPurposes, &out.ExternalDNSForPurposes + *out = make([]ExternalDNSPurposeConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSServiceConfigSpec. +func (in *DNSServiceConfigSpec) DeepCopy() *DNSServiceConfigSpec { + if in == nil { + return nil + } + out := new(DNSServiceConfigSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalDNSPurposeConfig) DeepCopyInto(out *ExternalDNSPurposeConfig) { + *out = *in + if in.PurposeSelector != nil { + in, out := &in.PurposeSelector, &out.PurposeSelector + *out = new(PurposeSelector) + (*in).DeepCopyInto(*out) + } + in.HelmValues.DeepCopyInto(&out.HelmValues) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalDNSPurposeConfig. +func (in *ExternalDNSPurposeConfig) DeepCopy() *ExternalDNSPurposeConfig { + if in == nil { + return nil + } + out := new(ExternalDNSPurposeConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalDNSSource) DeepCopyInto(out *ExternalDNSSource) { + *out = *in + if in.Helm != nil { + in, out := &in.Helm, &out.Helm + *out = new(apiv1.HelmRepositorySpec) + (*in).DeepCopyInto(*out) + } + if in.Git != nil { + in, out := &in.Git, &out.Git + *out = new(apiv1.GitRepositorySpec) + (*in).DeepCopyInto(*out) + } + if in.OCI != nil { + in, out := &in.OCI, &out.OCI + *out = new(apiv1.OCIRepositorySpec) + (*in).DeepCopyInto(*out) + } + if in.CopyAuthSecret != nil { + in, out := &in.CopyAuthSecret, &out.CopyAuthSecret + *out = new(SecretCopy) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalDNSSource. +func (in *ExternalDNSSource) DeepCopy() *ExternalDNSSource { + if in == nil { + return nil + } + out := new(ExternalDNSSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PurposeSelector) DeepCopyInto(out *PurposeSelector) { + *out = *in + in.PurposeSelectorRequirement.DeepCopyInto(&out.PurposeSelectorRequirement) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PurposeSelector. +func (in *PurposeSelector) DeepCopy() *PurposeSelector { + if in == nil { + return nil + } + out := new(PurposeSelector) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PurposeSelectorRequirement) DeepCopyInto(out *PurposeSelectorRequirement) { + *out = *in + if in.And != nil { + in, out := &in.And, &out.And + *out = make([]PurposeSelectorRequirement, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Or != nil { + in, out := &in.Or, &out.Or + *out = make([]PurposeSelectorRequirement, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Not != nil { + in, out := &in.Not, &out.Not + *out = new(PurposeSelectorRequirement) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PurposeSelectorRequirement. +func (in *PurposeSelectorRequirement) DeepCopy() *PurposeSelectorRequirement { + if in == nil { + return nil + } + out := new(PurposeSelectorRequirement) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretCopy) DeepCopyInto(out *SecretCopy) { + *out = *in + out.Source = in.Source + if in.Target != nil { + in, out := &in.Target, &out.Target + *out = new(common.ObjectReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretCopy. +func (in *SecretCopy) DeepCopy() *SecretCopy { + if in == nil { + return nil + } + out := new(SecretCopy) + in.DeepCopyInto(out) + return out +} diff --git a/api/go.mod b/api/go.mod new file mode 100644 index 0000000..190bd13 --- /dev/null +++ b/api/go.mod @@ -0,0 +1,36 @@ +module github.com/openmcp-project/platform-service-dns/api + +go 1.25.1 + +require ( + github.com/fluxcd/source-controller/api v1.6.2 + github.com/openmcp-project/openmcp-operator/api v0.14.0 + k8s.io/apiextensions-apiserver v0.34.0 + k8s.io/apimachinery v0.34.0 + k8s.io/client-go v0.34.0 + sigs.k8s.io/controller-runtime v0.22.1 +) + +require ( + github.com/fluxcd/pkg/apis/acl v0.7.0 // indirect + github.com/fluxcd/pkg/apis/meta v1.12.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/x448/float16 v0.8.4 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/text v0.28.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + k8s.io/api v0.34.0 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) diff --git a/api/go.sum b/api/go.sum new file mode 100644 index 0000000..827b2cc --- /dev/null +++ b/api/go.sum @@ -0,0 +1,125 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/fluxcd/pkg/apis/acl v0.7.0 h1:dMhZJH+g6ZRPjs4zVOAN9vHBd1DcavFgcIFkg5ooOE0= +github.com/fluxcd/pkg/apis/acl v0.7.0/go.mod h1:uv7pXXR/gydiX4MUwlQa7vS8JONEDztynnjTvY3JxKQ= +github.com/fluxcd/pkg/apis/meta v1.12.0 h1:XW15TKZieC2b7MN8VS85stqZJOx+/b8jATQ/xTUhVYg= +github.com/fluxcd/pkg/apis/meta v1.12.0/go.mod h1:+son1Va60x2eiDcTwd7lcctbI6C+K3gM7R+ULmEq1SI= +github.com/fluxcd/source-controller/api v1.6.2 h1:UmodAeqLIeF29HdTqf2GiacZyO+hJydJlepDaYsMvhc= +github.com/fluxcd/source-controller/api v1.6.2/go.mod h1:ZJcAi0nemsnBxjVgmJl0WQzNvB0rMETxQMTdoFosmMw= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +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= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= +github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= +github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/openmcp-project/openmcp-operator/api v0.14.0 h1:/Ogg13b/IBBmAVQEuFnOKvHuVV3o2DlrQpSQhXca0+g= +github.com/openmcp-project/openmcp-operator/api v0.14.0/go.mod h1:mgckJa59TpgTjta8+BWHwbOxlY1zVasqQTDFQLCs6M8= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +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/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.34.0 h1:L+JtP2wDbEYPUeNGbeSa/5GwFtIA662EmT2YSLOkAVE= +k8s.io/api v0.34.0/go.mod h1:YzgkIzOOlhl9uwWCZNqpw6RJy9L2FK4dlJeayUoydug= +k8s.io/apiextensions-apiserver v0.34.0 h1:B3hiB32jV7BcyKcMU5fDaDxk882YrJ1KU+ZSkA9Qxoc= +k8s.io/apiextensions-apiserver v0.34.0/go.mod h1:hLI4GxE1BDBy9adJKxUxCEHBGZtGfIg98Q+JmTD7+g0= +k8s.io/apimachinery v0.34.0 h1:eR1WO5fo0HyoQZt1wdISpFDffnWOvFLOOeJ7MgIv4z0= +k8s.io/apimachinery v0.34.0/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.0 h1:YoWv5r7bsBfb0Hs2jh8SOvFbKzzxyNo0nSb0zC19KZo= +k8s.io/client-go v0.34.0/go.mod h1:ozgMnEKXkRjeMvBZdV1AijMHLTh3pbACPvK7zFR+QQY= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d h1:wAhiDyZ4Tdtt7e46e9M5ZSAJ/MnPGPs+Ki1gHw4w1R0= +k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.22.1 h1:Ah1T7I+0A7ize291nJZdS1CabF/lB4E++WizgV24Eqg= +sigs.k8s.io/controller-runtime v0.22.1/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/api/install/install.go b/api/install/install.go new file mode 100644 index 0000000..eb4cfe4 --- /dev/null +++ b/api/install/install.go @@ -0,0 +1,26 @@ +package install + +import ( + apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + + dnsv1alpha1 "github.com/openmcp-project/platform-service-dns/api/dns/v1alpha1" +) + +// InstallCRDAPIs installs the CRD APIs in the scheme. +// This is used for the init subcommand. +func InstallCRDAPIs(scheme *runtime.Scheme) *runtime.Scheme { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(apiextv1.AddToScheme(scheme)) + + return scheme +} + +func InstallOperatorAPIsPlatform(scheme *runtime.Scheme) *runtime.Scheme { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(dnsv1alpha1.AddToScheme(scheme)) + + return scheme +} diff --git a/cmd/platform-service-dns/app/app.go b/cmd/platform-service-dns/app/app.go new file mode 100644 index 0000000..df60e42 --- /dev/null +++ b/cmd/platform-service-dns/app/app.go @@ -0,0 +1,81 @@ +package app + +import ( + "fmt" + "os" + + ctrl "sigs.k8s.io/controller-runtime" + + "github.com/spf13/cobra" + + "github.com/openmcp-project/controller-utils/pkg/clusters" + "github.com/openmcp-project/controller-utils/pkg/logging" +) + +func NewPlatformServiceDNSCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "platform-service-dns", + Short: "platform-service-dns does stuff with DNS", // TODO + } + cmd.SetOut(os.Stdout) + cmd.SetErr(os.Stderr) + + so := &SharedOptions{ + RawSharedOptions: &RawSharedOptions{}, + PlatformCluster: clusters.New("platform"), + } + so.AddPersistentFlags(cmd) + cmd.AddCommand(NewInitCommand(so)) + cmd.AddCommand(NewRunCommand(so)) + + return cmd +} + +type RawSharedOptions struct { + Environment string `json:"environment"` + DryRun bool `json:"dry-run"` +} + +type SharedOptions struct { + *RawSharedOptions + PlatformCluster *clusters.Cluster + + // fields filled in Complete() + Log logging.Logger + ProviderName string +} + +func (o *SharedOptions) AddPersistentFlags(cmd *cobra.Command) { + // logging + logging.InitFlags(cmd.PersistentFlags()) + // clusters + o.PlatformCluster.RegisterSingleConfigPathFlag(cmd.PersistentFlags()) + // environment + cmd.PersistentFlags().StringVar(&o.Environment, "environment", "", "Environment name. Required. This is used to distinguish between different environments that are watching the same Onboarding cluster. Must be globally unique.") + cmd.PersistentFlags().BoolVar(&o.DryRun, "dry-run", false, "If set, the command aborts after evaluation of the given flags.") +} + +func (o *SharedOptions) Complete() error { + if o.Environment == "" { + return fmt.Errorf("environment must not be empty") + } + + o.ProviderName = os.Getenv("OPENMCP_PROVIDER_NAME") + if o.ProviderName == "" { + o.ProviderName = "gardener" + } + + // build logger + log, err := logging.GetLogger() + if err != nil { + return err + } + o.Log = log + ctrl.SetLogger(o.Log.Logr()) + + if err := o.PlatformCluster.InitializeRESTConfig(); err != nil { + return err + } + + return nil +} diff --git a/cmd/platform-service-dns/app/init.go b/cmd/platform-service-dns/app/init.go new file mode 100644 index 0000000..6db9b98 --- /dev/null +++ b/cmd/platform-service-dns/app/init.go @@ -0,0 +1,73 @@ +package app + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + // crdutil "github.com/openmcp-project/controller-utils/pkg/crds" + // clustersv1alpha1 "github.com/openmcp-project/platform-service-dns/api/clusters/v1alpha1" + // openmcpconst "github.com/openmcp-project/openmcp-operator/api/constants" +) + +func NewInitCommand(so *SharedOptions) *cobra.Command { + opts := &InitOptions{ + SharedOptions: so, + } + cmd := &cobra.Command{ + Use: "init", + Short: "Initialize Platform Service DNS", + Run: func(cmd *cobra.Command, args []string) { + opts.PrintRawOptions(cmd) + if err := opts.Complete(cmd.Context()); err != nil { + panic(fmt.Errorf("error completing options: %w", err)) + } + opts.PrintCompletedOptions(cmd) + if opts.DryRun { + cmd.Println("=== END OF DRY RUN ===") + return + } + if err := opts.Run(cmd.Context()); err != nil { + panic(err) + } + }, + } + opts.AddFlags(cmd) + + return cmd +} + +type InitOptions struct { + *SharedOptions +} + +func (o *InitOptions) AddFlags(cmd *cobra.Command) {} + +func (o *InitOptions) Complete(ctx context.Context) error { + if err := o.SharedOptions.Complete(); err != nil { + return err + } + + return nil +} + +func (o *InitOptions) Run(ctx context.Context) error { + // if err := o.PlatformCluster.InitializeClient(providerscheme.InstallCRDAPIs(runtime.NewScheme())); err != nil { + // return err + // } + + log := o.Log.WithName("main") + log.Info("Environment", "value", o.Environment) + log.Info("ProviderName", "value", o.ProviderName) + + // apply CRDs + // TODO: are CRDs required? + // crdManager := crdutil.NewCRDManager(openmcpconst.ClusterLabel, crds.CRDs) + // crdManager.AddCRDLabelToClusterMapping(clustersv1alpha1.PURPOSE_PLATFORM, o.PlatformCluster) + // if err := crdManager.CreateOrUpdateCRDs(ctx, &log); err != nil { + // return fmt.Errorf("error creating/updating CRDs: %w", err) + // } + + log.Info("Finished init command") + return nil +} diff --git a/cmd/platform-service-dns/app/print.go b/cmd/platform-service-dns/app/print.go new file mode 100644 index 0000000..175edbd --- /dev/null +++ b/cmd/platform-service-dns/app/print.go @@ -0,0 +1,84 @@ +package app + +import ( + "fmt" + + "sigs.k8s.io/yaml" + + "github.com/spf13/cobra" +) + +func (o *SharedOptions) PrintRaw(cmd *cobra.Command) { + data, err := yaml.Marshal(o.RawSharedOptions) + if err != nil { + cmd.Println(fmt.Errorf("error marshalling raw shared options: %w", err).Error()) + return + } + cmd.Print(string(data)) +} + +func (o *SharedOptions) PrintCompleted(cmd *cobra.Command) { + raw := map[string]any{ + "platformCluster": o.PlatformCluster.APIServerEndpoint(), + "providerName": o.ProviderName, + } + data, err := yaml.Marshal(raw) + if err != nil { + cmd.Println(fmt.Errorf("error marshalling completed shared options: %w", err).Error()) + return + } + cmd.Print(string(data)) +} + +func (o *InitOptions) PrintRaw(cmd *cobra.Command) {} + +func (o *InitOptions) PrintRawOptions(cmd *cobra.Command) { + cmd.Println("########## RAW OPTIONS START ##########") + o.SharedOptions.PrintRaw(cmd) + o.PrintRaw(cmd) + cmd.Println("########## RAW OPTIONS END ##########") +} + +func (o *InitOptions) PrintCompleted(cmd *cobra.Command) {} + +func (o *InitOptions) PrintCompletedOptions(cmd *cobra.Command) { + cmd.Println("########## COMPLETED OPTIONS START ##########") + o.SharedOptions.PrintCompleted(cmd) + o.PrintCompleted(cmd) + cmd.Println("########## COMPLETED OPTIONS END ##########") +} + +func (o *RunOptions) PrintRaw(cmd *cobra.Command) { + data, err := yaml.Marshal(o.RawRunOptions) + if err != nil { + cmd.Println(fmt.Errorf("error marshalling raw options: %w", err).Error()) + return + } + cmd.Print(string(data)) +} + +func (o *RunOptions) PrintRawOptions(cmd *cobra.Command) { + cmd.Println("########## RAW OPTIONS START ##########") + o.SharedOptions.PrintRaw(cmd) + o.PrintRaw(cmd) + cmd.Println("########## RAW OPTIONS END ##########") +} + +func (o *RunOptions) PrintCompleted(cmd *cobra.Command) { + raw := map[string]any{ + // TODO add options or remove + } + data, err := yaml.Marshal(raw) + if err != nil { + cmd.Println(fmt.Errorf("error marshalling completed options: %w", err).Error()) + return + } + cmd.Print(string(data)) +} + +func (o *RunOptions) PrintCompletedOptions(cmd *cobra.Command) { + cmd.Println("########## COMPLETED OPTIONS START ##########") + o.SharedOptions.PrintCompleted(cmd) + o.PrintCompleted(cmd) + cmd.Println("########## COMPLETED OPTIONS END ##########") +} diff --git a/cmd/platform-service-dns/app/run.go b/cmd/platform-service-dns/app/run.go new file mode 100644 index 0000000..cb818a1 --- /dev/null +++ b/cmd/platform-service-dns/app/run.go @@ -0,0 +1,253 @@ +package app + +import ( + "context" + "crypto/tls" + "fmt" + "path/filepath" + + "github.com/spf13/cobra" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/certwatcher" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/metrics/filters" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/webhook" + + "github.com/openmcp-project/controller-utils/pkg/logging" +) + +var setupLog logging.Logger + +func NewRunCommand(so *SharedOptions) *cobra.Command { + opts := &RunOptions{ + SharedOptions: so, + } + cmd := &cobra.Command{ + Use: "run", + Short: "Run the Platform Service DNS", + Run: func(cmd *cobra.Command, args []string) { + opts.PrintRawOptions(cmd) + if err := opts.Complete(cmd.Context()); err != nil { + panic(fmt.Errorf("error completing options: %w", err)) + } + opts.PrintCompletedOptions(cmd) + if opts.DryRun { + cmd.Println("=== END OF DRY RUN ===") + return + } + if err := opts.Run(cmd.Context()); err != nil { + panic(err) + } + }, + } + opts.AddFlags(cmd) + + return cmd +} + +type RawRunOptions struct { + // kubebuilder default flags + MetricsAddr string `json:"metrics-bind-address"` + MetricsCertPath string `json:"metrics-cert-path"` + MetricsCertName string `json:"metrics-cert-name"` + MetricsCertKey string `json:"metrics-cert-key"` + WebhookCertPath string `json:"webhook-cert-path"` + WebhookCertName string `json:"webhook-cert-name"` + WebhookCertKey string `json:"webhook-cert-key"` + EnableLeaderElection bool `json:"leader-elect"` + ProbeAddr string `json:"health-probe-bind-address"` + PprofAddr string `json:"pprof-bind-address"` + SecureMetrics bool `json:"metrics-secure"` + EnableHTTP2 bool `json:"enable-http2"` + + Controllers []string `json:"controllers"` +} + +type RunOptions struct { + *SharedOptions + RawRunOptions + + // fields filled in Complete() + TLSOpts []func(*tls.Config) + WebhookTLSOpts []func(*tls.Config) + MetricsServerOptions metricsserver.Options + MetricsCertWatcher *certwatcher.CertWatcher + WebhookCertWatcher *certwatcher.CertWatcher +} + +func (o *RunOptions) AddFlags(cmd *cobra.Command) { + // kubebuilder default flags + cmd.Flags().StringVar(&o.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.") + cmd.Flags().StringVar(&o.ProbeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") + cmd.Flags().StringVar(&o.PprofAddr, "pprof-bind-address", "", "The address the pprof endpoint binds to. Expected format is ':'. Leave empty to disable pprof endpoint.") + cmd.Flags().BoolVar(&o.EnableLeaderElection, "leader-elect", false, "Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.") + cmd.Flags().BoolVar(&o.SecureMetrics, "metrics-secure", true, "If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.") + cmd.Flags().StringVar(&o.WebhookCertPath, "webhook-cert-path", "", "The directory that contains the webhook certificate.") + cmd.Flags().StringVar(&o.WebhookCertName, "webhook-cert-name", "tls.crt", "The name of the webhook certificate file.") + cmd.Flags().StringVar(&o.WebhookCertKey, "webhook-cert-key", "tls.key", "The name of the webhook key file.") + cmd.Flags().StringVar(&o.MetricsCertPath, "metrics-cert-path", "", "The directory that contains the metrics server certificate.") + cmd.Flags().StringVar(&o.MetricsCertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.") + cmd.Flags().StringVar(&o.MetricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.") + cmd.Flags().BoolVar(&o.EnableHTTP2, "enable-http2", false, "If set, HTTP/2 will be enabled for the metrics and webhook servers") +} + +func (o *RunOptions) Complete(ctx context.Context) error { + if err := o.SharedOptions.Complete(); err != nil { + return err + } + setupLog = o.Log.WithName("setup") + ctrl.SetLogger(o.Log.Logr()) + + // kubebuilder default stuff + + // 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 + // Rapid Reset CVEs. For more information see: + // - https://github.com/advisories/GHSA-qppj-fm5r-hxr3 + // - https://github.com/advisories/GHSA-4374-p667-p6c8 + disableHTTP2 := func(c *tls.Config) { + setupLog.Info("Disabling http/2") + c.NextProtos = []string{"http/1.1"} + } + + if !o.EnableHTTP2 { + o.TLSOpts = append(o.TLSOpts, disableHTTP2) + } + + // Initial webhook TLS options + o.WebhookTLSOpts = o.TLSOpts + + if len(o.WebhookCertPath) > 0 { + setupLog.Info("Initializing webhook certificate watcher using provided certificates", "webhook-cert-path", o.WebhookCertPath, "webhook-cert-name", o.WebhookCertName, "webhook-cert-key", o.WebhookCertKey) + + var err error + o.WebhookCertWatcher, err = certwatcher.New( + filepath.Join(o.WebhookCertPath, o.WebhookCertName), + filepath.Join(o.WebhookCertPath, o.WebhookCertKey), + ) + if err != nil { + return fmt.Errorf("failed to initialize webhook certificate watcher: %w", err) + } + + o.WebhookTLSOpts = append(o.WebhookTLSOpts, func(config *tls.Config) { + config.GetCertificate = o.WebhookCertWatcher.GetCertificate + }) + } + + // More info: + // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.2/pkg/metrics/server + // - https://book.kubebuilder.io/reference/metrics.html + o.MetricsServerOptions = metricsserver.Options{ + BindAddress: o.MetricsAddr, + SecureServing: o.SecureMetrics, + TLSOpts: o.TLSOpts, + } + + if o.SecureMetrics { + // FilterProvider is used to protect the metrics endpoint with authn/authz. + // These configurations ensure that only authorized users and service accounts + // can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info: + // https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.2/pkg/metrics/filters#WithAuthenticationAndAuthorization + o.MetricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization + } + + // If the certificate is not specified, controller-runtime will automatically + // generate self-signed certificates for the metrics server. While convenient for development and testing, + // this setup is not recommended for production. + // + // TODO(user): If you enable certManager, uncomment the following lines: + // - [METRICS-WITH-CERTS] at config/default/kustomization.yaml to generate and use certificates + // managed by cert-manager for the metrics server. + // - [PROMETHEUS-WITH-CERTS] at config/prometheus/kustomization.yaml for TLS certification. + if len(o.MetricsCertPath) > 0 { + setupLog.Info("Initializing metrics certificate watcher using provided certificates", "metrics-cert-path", o.MetricsCertPath, "metrics-cert-name", o.MetricsCertName, "metrics-cert-key", o.MetricsCertKey) + + var err error + o.MetricsCertWatcher, err = certwatcher.New( + filepath.Join(o.MetricsCertPath, o.MetricsCertName), + filepath.Join(o.MetricsCertPath, o.MetricsCertKey), + ) + if err != nil { + return fmt.Errorf("failed to initialize metrics certificate watcher: %w", err) + } + + o.MetricsServerOptions.TLSOpts = append(o.MetricsServerOptions.TLSOpts, func(config *tls.Config) { + config.GetCertificate = o.MetricsCertWatcher.GetCertificate + }) + } + + return nil +} + +func (o *RunOptions) Run(ctx context.Context) error { + // TODO: CRDs required? + // if err := o.PlatformCluster.InitializeClient(providerscheme.InstallProviderAPIs(runtime.NewScheme())); err != nil { + // return err + // } + + setupLog = o.Log.WithName("setup") + setupLog.Info("Environment", "value", o.Environment) + setupLog.Info("ProviderName", "value", o.ProviderName) + + webhookServer := webhook.NewServer(webhook.Options{ + TLSOpts: o.WebhookTLSOpts, + }) + + mgr, err := ctrl.NewManager(o.PlatformCluster.RESTConfig(), ctrl.Options{ + Scheme: runtime.NewScheme(), // TODO add scheme, e.g. providerscheme.InstallProviderAPIs(runtime.NewScheme()), + Metrics: o.MetricsServerOptions, + WebhookServer: webhookServer, + HealthProbeBindAddress: o.ProbeAddr, + PprofBindAddress: o.PprofAddr, + LeaderElection: o.EnableLeaderElection, + LeaderElectionID: "github.com/openmcp-project/platform-service-dns", + // 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 + // speeds up voluntary leader transitions as the new leader don't have to wait + // LeaseDuration time first. + // + // In the default scaffold provided, the program ends immediately after + // the manager stops, so would be fine to enable this option. However, + // if you are doing or is intended to do any operation such as perform cleanups + // after the manager stops then its usage might be unsafe. + // LeaderElectionReleaseOnCancel: true, + }) + if err != nil { + return fmt.Errorf("unable to create manager: %w", err) + } + + // TODO setup controllers + + if o.MetricsCertWatcher != nil { + setupLog.Info("Adding metrics certificate watcher to manager") + if err := mgr.Add(o.MetricsCertWatcher); err != nil { + return fmt.Errorf("unable to add metrics certificate watcher to manager: %w", err) + } + } + + if o.WebhookCertWatcher != nil { + setupLog.Info("Adding webhook certificate watcher to manager") + if err := mgr.Add(o.WebhookCertWatcher); err != nil { + return fmt.Errorf("unable to add webhook certificate watcher to manager: %w", err) + } + } + + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { + return fmt.Errorf("unable to set up health check: %w", err) + } + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { + return fmt.Errorf("unable to set up ready check: %w", err) + } + + setupLog.Info("Starting manager") + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + return fmt.Errorf("problem running manager: %w", err) + } + + return nil +} diff --git a/cmd/platform-service-dns/main.go b/cmd/platform-service-dns/main.go new file mode 100644 index 0000000..26bc0c4 --- /dev/null +++ b/cmd/platform-service-dns/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "fmt" + "os" + + "github.com/openmcp-project/platform-service-dns/cmd/platform-service-dns/app" +) + +func main() { + cmd := app.NewPlatformServiceDNSCommand() + + if err := cmd.Execute(); err != nil { + fmt.Print(err) + os.Exit(1) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..aade720 --- /dev/null +++ b/go.mod @@ -0,0 +1,104 @@ +module github.com/openmcp-project/platform-service-dns + +go 1.25.1 + +replace github.com/openmcp-project/platform-service-dns/api => ./api + +require ( + github.com/fluxcd/source-controller/api v1.6.2 + github.com/openmcp-project/controller-utils v0.19.0 + github.com/openmcp-project/openmcp-operator/api v0.14.0 + github.com/openmcp-project/openmcp-operator/lib v0.14.1-0.20250910074108-2166b543cee3 + github.com/openmcp-project/platform-service-dns/api v0.0.0-00010101000000-000000000000 + github.com/spf13/cobra v1.9.1 + k8s.io/api v0.34.0 + k8s.io/apimachinery v0.34.0 + k8s.io/client-go v0.34.0 + sigs.k8s.io/controller-runtime v0.22.1 + sigs.k8s.io/yaml v1.6.0 +) + +require ( + cel.dev/expr v0.24.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fluxcd/pkg/apis/acl v0.7.0 // indirect + github.com/fluxcd/pkg/apis/meta v1.12.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-logr/zapr v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/cel-go v0.26.0 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect + 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/mailru/easyjson v0.9.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.22.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/spf13/pflag v1.0.9 // indirect + github.com/stoewer/go-strcase v1.3.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/sdk v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.opentelemetry.io/proto/otlp v1.5.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/oauth2 v0.27.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/term v0.34.0 // indirect + golang.org/x/text v0.28.0 // indirect + golang.org/x/time v0.10.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect + google.golang.org/grpc v1.72.1 // indirect + google.golang.org/protobuf v1.36.7 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apiextensions-apiserver v0.34.0 // indirect + k8s.io/apiserver v0.34.0 // indirect + k8s.io/component-base v0.34.0 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect + k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d // indirect + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..935a7fb --- /dev/null +++ b/go.sum @@ -0,0 +1,272 @@ +cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +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/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fluxcd/pkg/apis/acl v0.7.0 h1:dMhZJH+g6ZRPjs4zVOAN9vHBd1DcavFgcIFkg5ooOE0= +github.com/fluxcd/pkg/apis/acl v0.7.0/go.mod h1:uv7pXXR/gydiX4MUwlQa7vS8JONEDztynnjTvY3JxKQ= +github.com/fluxcd/pkg/apis/meta v1.12.0 h1:XW15TKZieC2b7MN8VS85stqZJOx+/b8jATQ/xTUhVYg= +github.com/fluxcd/pkg/apis/meta v1.12.0/go.mod h1:+son1Va60x2eiDcTwd7lcctbI6C+K3gM7R+ULmEq1SI= +github.com/fluxcd/source-controller/api v1.6.2 h1:UmodAeqLIeF29HdTqf2GiacZyO+hJydJlepDaYsMvhc= +github.com/fluxcd/source-controller/api v1.6.2/go.mod h1:ZJcAi0nemsnBxjVgmJl0WQzNvB0rMETxQMTdoFosmMw= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI= +github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY= +github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +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/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +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.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +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= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.25.3 h1:Ty8+Yi/ayDAGtk4XxmmfUy4GabvM+MegeB4cDLRi6nw= +github.com/onsi/ginkgo/v2 v2.25.3/go.mod h1:43uiyQC4Ed2tkOzLsEYm7hnrb7UJTWHYNsuy3bG/snE= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= +github.com/openmcp-project/controller-utils v0.19.0 h1:D4Ht3LI/Ue5yk2wdAnJEpChUVmB6xM7kglwhn7a2J3g= +github.com/openmcp-project/controller-utils v0.19.0/go.mod h1:zxcbcmedLdlQ//X/nwdPvq/nM3ikyR13DbOivou2I4Y= +github.com/openmcp-project/openmcp-operator/api v0.14.0 h1:/Ogg13b/IBBmAVQEuFnOKvHuVV3o2DlrQpSQhXca0+g= +github.com/openmcp-project/openmcp-operator/api v0.14.0/go.mod h1:mgckJa59TpgTjta8+BWHwbOxlY1zVasqQTDFQLCs6M8= +github.com/openmcp-project/openmcp-operator/lib v0.14.1-0.20250910074108-2166b543cee3 h1:RJmtE7Iqt8LS2ruGwJed6CqEoVgO1XoPQdDrnDwcaI8= +github.com/openmcp-project/openmcp-operator/lib v0.14.1-0.20250910074108-2166b543cee3/go.mod h1:q3vb/BR9idvd0akkKHsjA5WkR38Lj378F8iFgiOSWs0= +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= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= +github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= +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 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +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.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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= +go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +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/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0= +golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +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.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= +golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= +golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= +google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= +google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= +google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.34.0 h1:L+JtP2wDbEYPUeNGbeSa/5GwFtIA662EmT2YSLOkAVE= +k8s.io/api v0.34.0/go.mod h1:YzgkIzOOlhl9uwWCZNqpw6RJy9L2FK4dlJeayUoydug= +k8s.io/apiextensions-apiserver v0.34.0 h1:B3hiB32jV7BcyKcMU5fDaDxk882YrJ1KU+ZSkA9Qxoc= +k8s.io/apiextensions-apiserver v0.34.0/go.mod h1:hLI4GxE1BDBy9adJKxUxCEHBGZtGfIg98Q+JmTD7+g0= +k8s.io/apimachinery v0.34.0 h1:eR1WO5fo0HyoQZt1wdISpFDffnWOvFLOOeJ7MgIv4z0= +k8s.io/apimachinery v0.34.0/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apiserver v0.34.0 h1:Z51fw1iGMqN7uJ1kEaynf2Aec1Y774PqU+FVWCFV3Jg= +k8s.io/apiserver v0.34.0/go.mod h1:52ti5YhxAvewmmpVRqlASvaqxt0gKJxvCeW7ZrwgazQ= +k8s.io/client-go v0.34.0 h1:YoWv5r7bsBfb0Hs2jh8SOvFbKzzxyNo0nSb0zC19KZo= +k8s.io/client-go v0.34.0/go.mod h1:ozgMnEKXkRjeMvBZdV1AijMHLTh3pbACPvK7zFR+QQY= +k8s.io/component-base v0.34.0 h1:bS8Ua3zlJzapklsB1dZgjEJuJEeHjj8yTu1gxE2zQX8= +k8s.io/component-base v0.34.0/go.mod h1:RSCqUdvIjjrEm81epPcjQ/DS+49fADvGSCkIP3IC6vg= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= +k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d h1:wAhiDyZ4Tdtt7e46e9M5ZSAJ/MnPGPs+Ki1gHw4w1R0= +k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= +sigs.k8s.io/controller-runtime v0.22.1 h1:Ah1T7I+0A7ize291nJZdS1CabF/lB4E++WizgV24Eqg= +sigs.k8s.io/controller-runtime v0.22.1/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/hack/common b/hack/common new file mode 160000 index 0000000..acbe270 --- /dev/null +++ b/hack/common @@ -0,0 +1 @@ +Subproject commit acbe27054cdc9d5ef16b443ff9d910fd31863f32 diff --git a/internal/controllers/cluster/controller.go b/internal/controllers/cluster/controller.go new file mode 100644 index 0000000..8fb50a1 --- /dev/null +++ b/internal/controllers/cluster/controller.go @@ -0,0 +1,376 @@ +package cluster + +import ( + "context" + "fmt" + "maps" + "strings" + + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + fluxv1 "github.com/fluxcd/source-controller/api/v1" + + "github.com/openmcp-project/controller-utils/pkg/clusters" + "github.com/openmcp-project/controller-utils/pkg/collections" + maputils "github.com/openmcp-project/controller-utils/pkg/collections/maps" + ctrlutils "github.com/openmcp-project/controller-utils/pkg/controller" + errutils "github.com/openmcp-project/controller-utils/pkg/errors" + "github.com/openmcp-project/controller-utils/pkg/logging" + + clustersv1alpha1 "github.com/openmcp-project/openmcp-operator/api/clusters/v1alpha1" + clusterconst "github.com/openmcp-project/openmcp-operator/api/clusters/v1alpha1/constants" + commonapi "github.com/openmcp-project/openmcp-operator/api/common" + openmcpconst "github.com/openmcp-project/openmcp-operator/api/constants" + accesslib "github.com/openmcp-project/openmcp-operator/lib/clusteraccess" + + dnsv1alpha1 "github.com/openmcp-project/platform-service-dns/api/dns/v1alpha1" +) + +const ControllerName = "DNSCluster" + +type ClusterReconciler struct { + PlatformCluster *clusters.Cluster + Config *dnsv1alpha1.DNSServiceConfig + eventRecorder record.EventRecorder + ProviderName string + ProviderNamespace string +} + +func NewClusterReconciler(platformCluster *clusters.Cluster, recorder record.EventRecorder, cfg *dnsv1alpha1.DNSServiceConfig, providerName, providerNamespace string) *ClusterReconciler { + return &ClusterReconciler{ + PlatformCluster: platformCluster, + eventRecorder: recorder, + ProviderName: providerName, + ProviderNamespace: providerNamespace, + Config: cfg, + } +} + +var _ reconcile.Reconciler = &ClusterReconciler{} + +type ReconcileResult struct { + Result reconcile.Result + ReconcileError errutils.ReasonableError + Config *dnsv1alpha1.ExternalDNSPurposeConfig + ConfigIndex int +} + +func (r *ClusterReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + log := logging.FromContextOrPanic(ctx).WithName(ControllerName) + ctx = logging.NewContext(ctx, log) + log.Info("Starting reconcile") + rr := r.reconcile(ctx, req) + + // no status update, because the Cluster resource doesn't have status fields for DNS configuration + // instead, output events for significant changes + // TODO + + return rr.Result, rr.ReconcileError +} + +func (r *ClusterReconciler) reconcile(ctx context.Context, req reconcile.Request) ReconcileResult { + log := logging.FromContextOrPanic(ctx) + + // get Cluster resource + c := &clustersv1alpha1.Cluster{} + if err := r.PlatformCluster.Client().Get(ctx, req.NamespacedName, c); err != nil { + if apierrors.IsNotFound(err) { + log.Info("Resource not found") + return ReconcileResult{} + } + return ReconcileResult{ReconcileError: errutils.WithReason(fmt.Errorf("unable to get resource '%s' from cluster: %w", req.String(), err), clusterconst.ReasonPlatformClusterInteractionProblem)} + } + + // handle operation annotation + if c.GetAnnotations() != nil { + op, ok := c.GetAnnotations()[dnsv1alpha1.OperationAnnotation] + if !ok { + // only evaluate the generic operation annotation if no DNS-specific one is set + op, ok = c.GetAnnotations()[openmcpconst.OperationAnnotation] + } + if ok { + switch op { + case openmcpconst.OperationAnnotationValueIgnore: + log.Info("Ignoring resource due to ignore operation annotation") + return ReconcileResult{} + case openmcpconst.OperationAnnotationValueReconcile: + log.Debug("Removing reconcile operation annotation from resource") + if err := ctrlutils.EnsureAnnotation(ctx, r.PlatformCluster.Client(), c, openmcpconst.OperationAnnotation, "", true, ctrlutils.DELETE); err != nil { + return ReconcileResult{ReconcileError: errutils.WithReason(fmt.Errorf("error removing operation annotation: %w", err), clusterconst.ReasonPlatformClusterInteractionProblem)} + } + } + } + } + + rr := ReconcileResult{} + expectedLabels := map[string]string{ + openmcpconst.ManagedByLabel: ControllerName, + openmcpconst.ManagedPurposeLabel: c.Name, + } + + if c.DeletionTimestamp.IsZero() { + // CREATE/UPDATE + log.Info("Creating or updating DNS configuration for Cluster") + + // add finalizer to Cluster if not present + old := c.DeepCopy() + if controllerutil.AddFinalizer(c, dnsv1alpha1.ExternalDNSFinalizerOnCluster) { + log.Info("Adding finalizer to Cluster", "finalizer", dnsv1alpha1.ExternalDNSFinalizerOnCluster) + if err := r.PlatformCluster.Client().Patch(ctx, c, client.MergeFrom(old)); err != nil { + rr.ReconcileError = errutils.WithReason(fmt.Errorf("error adding finalizer to Cluster '%s/%s': %w", c.Namespace, c.Name, err), clusterconst.ReasonPlatformClusterInteractionProblem) + return rr + } + } + + // iterate over configurations with purpose selectors and choose the first matching one + for i, cfg := range r.Config.Spec.ExternalDNSForPurposes { + if cfg.PurposeSelector == nil || cfg.PurposeSelector.Matches(c.Spec.Purposes) { + log.Info("Found configuration with matching purpose selector", "configName", cfg.Name, "configIndex", i) + rr.Config = &r.Config.Spec.ExternalDNSForPurposes[i] + rr.ConfigIndex = i + break + } + } + if rr.Config == nil { + log.Info("No configuration with matching purpose selector found") + rr.ConfigIndex = -1 + return rr + } + + // get access to the Cluster + accessMgr := accesslib.NewClusterAccessManager(r.PlatformCluster.Client(), strings.ToLower(ControllerName), c.Namespace) + localName := c.Name + if len(ControllerName+"--"+localName) > 63 { + localName = ctrlutils.K8sNameUUIDUnsafe(c.Name) + } + // TODO: use access + _, ar, err := accessMgr.WaitForClusterAccess(ctx, localName, nil, &commonapi.ObjectReference{ + Name: c.Name, + Namespace: c.Namespace, + }, accesslib.ReferenceToCluster, []clustersv1alpha1.PermissionsRequest{ + { + Rules: []rbacv1.PolicyRule{ // TODO: restrict permissions + { + APIGroups: []string{"*"}, + Resources: []string{"*"}, + Verbs: []string{"*"}, + }, + }, + }, + }) + if err != nil { + rr.ReconcileError = errutils.WithReason(fmt.Errorf("error getting access to Cluster '%s/%s': %w", c.Namespace, c.Name, err), clusterconst.ReasonInternalError) + return rr + } + + // inject labels into AccessRequest + if err := ctrlutils.EnsureLabel(ctx, r.PlatformCluster.Client(), ar, openmcpconst.ManagedByLabel, ControllerName, true, ctrlutils.OVERWRITE); err != nil { + rr.ReconcileError = errutils.WithReason(fmt.Errorf("error ensuring labels on AccessRequest: %w", err), clusterconst.ReasonPlatformClusterInteractionProblem) + return rr + } + + rr = r.deployAuthSecret(ctx, c, expectedLabels, rr) + if rr.ReconcileError != nil { + return rr + } + + rr = r.deployFluxSource(ctx, c, expectedLabels, rr) + if rr.ReconcileError != nil { + return rr + } + + // TODO: deploy Flux Kustomization to deploy external-dns with selected config onto Cluster + } else { + // DELETE + log.Info("Cluster marked for deletion, cleaning up DNS configuration") + + // TODO: clean up deployed resources by removing Flux resources with matching labels + + // TODO: clean up copied auth secret if it was copied + + // remove finalizer from Cluster + old := c.DeepCopy() + if controllerutil.RemoveFinalizer(c, dnsv1alpha1.ExternalDNSFinalizerOnCluster) { + log.Info("Removing finalizer from Cluster", "finalizer", dnsv1alpha1.ExternalDNSFinalizerOnCluster) + if err := r.PlatformCluster.Client().Patch(ctx, c, client.MergeFrom(old)); err != nil { + rr.ReconcileError = errutils.WithReason(fmt.Errorf("error removing finalizer from Cluster '%s/%s': %w", c.Namespace, c.Name, err), clusterconst.ReasonPlatformClusterInteractionProblem) + return rr + } + } + } + + return rr +} + +func (r *ClusterReconciler) deployAuthSecret(ctx context.Context, c *clustersv1alpha1.Cluster, expectedLabels map[string]string, rr ReconcileResult) ReconcileResult { + log := logging.FromContextOrPanic(ctx) + + // copy secret if configured + if r.Config.Spec.ExternalDNSSource.CopyAuthSecret != nil { + source := &corev1.Secret{} + source.Name = r.Config.Spec.ExternalDNSSource.CopyAuthSecret.Source.Name + source.Namespace = r.ProviderNamespace + log.Debug("Auth secret copying configured, getting source secret", "sourceNamespace", source.Namespace, "sourceName", source.Name) + if err := r.PlatformCluster.Client().Get(ctx, client.ObjectKeyFromObject(source), source); err != nil { + rr.ReconcileError = errutils.WithReason(fmt.Errorf("error getting source secret '%s/%s': %w", source.Namespace, source.Name, err), clusterconst.ReasonPlatformClusterInteractionProblem) + return rr + } + + // check if target secret already exists + target := &corev1.Secret{} + target.Name = source.Name + if r.Config.Spec.ExternalDNSSource.CopyAuthSecret.Target != nil && r.Config.Spec.ExternalDNSSource.CopyAuthSecret.Target.Name != "" { + target.Name = r.Config.Spec.ExternalDNSSource.CopyAuthSecret.Target.Name + } + target.Namespace = c.Namespace + targetExists := true + if err := r.PlatformCluster.Client().Get(ctx, client.ObjectKeyFromObject(target), target); err != nil { + if !apierrors.IsNotFound(err) { + rr.ReconcileError = errutils.WithReason(fmt.Errorf("error getting target secret '%s/%s': %w", target.Namespace, target.Name, err), clusterconst.ReasonPlatformClusterInteractionProblem) + return rr + } + targetExists = false + } + if targetExists { + // if target secret exists, verify that it is managed by us + log.Debug("Target secret already exists", "targetNamespace", target.Namespace, "targetName", target.Name) + for k, v := range expectedLabels { + if v2, ok := target.Labels[k]; !ok || v2 != v { + rr.ReconcileError = errutils.WithReason(fmt.Errorf("target secret '%s/%s' already exists and is not managed by %s controller", target.Namespace, target.Name, ControllerName), clusterconst.ReasonConfigurationProblem) + return rr + } + } + } + log.Debug("Creating or updating target secret", "targetNamespace", target.Namespace, "targetName", target.Name) + if _, err := controllerutil.CreateOrUpdate(ctx, r.PlatformCluster.Client(), target, func() error { + target.Labels = maputils.Merge(target.Labels, source.Labels, expectedLabels) + target.Annotations = maputils.Merge(target.Annotations, source.Annotations) + target.Data = make(map[string][]byte, len(source.Data)) + maps.Copy(target.Data, source.Data) + target.Type = source.Type + return nil + }); err != nil { + rr.ReconcileError = errutils.WithReason(fmt.Errorf("error creating or updating target secret '%s/%s': %w", target.Namespace, target.Name, err), clusterconst.ReasonPlatformClusterInteractionProblem) + return rr + } + } + + return rr +} + +func (r *ClusterReconciler) deployFluxSource(ctx context.Context, c *clustersv1alpha1.Cluster, expectedLabels map[string]string, rr ReconcileResult) ReconcileResult { + log := logging.FromContextOrPanic(ctx) + + // deploy Flux Source + var fluxSource client.Object + var setSpec func(obj client.Object) error + var kind string + sourceName := c.Name + ".external-dns" // TODO: shorten if too long + // list existing Flux sources to detect obsolete ones + existingHelm := &fluxv1.HelmRepositoryList{} + if err := r.PlatformCluster.Client().List(ctx, existingHelm, client.InNamespace(c.Namespace), client.MatchingLabels(expectedLabels)); err != nil { + rr.ReconcileError = errutils.WithReason(fmt.Errorf("error listing existing HelmRepository resources in target namespace '%s': %w", c.Namespace, err), clusterconst.ReasonPlatformClusterInteractionProblem) + return rr + } + existingGit := &fluxv1.GitRepositoryList{} + if err := r.PlatformCluster.Client().List(ctx, existingGit, client.InNamespace(c.Namespace), client.MatchingLabels(expectedLabels)); err != nil { + rr.ReconcileError = errutils.WithReason(fmt.Errorf("error listing existing GitRepository resources in target namespace '%s': %w", c.Namespace, err), clusterconst.ReasonPlatformClusterInteractionProblem) + return rr + } + existingOCI := &fluxv1.OCIRepositoryList{} + if err := r.PlatformCluster.Client().List(ctx, existingOCI, client.InNamespace(c.Namespace), client.MatchingLabels(expectedLabels)); err != nil { + rr.ReconcileError = errutils.WithReason(fmt.Errorf("error listing existing OCIRepository resources in target namespace '%s': %w", c.Namespace, err), clusterconst.ReasonPlatformClusterInteractionProblem) + return rr + } + toBeDeleted := []client.Object{} + // determine which type of source to create and which existing sources to delete + if r.Config.Spec.ExternalDNSSource.Helm != nil { + fluxSource = &fluxv1.HelmRepository{} + setSpec = func(obj client.Object) error { + helmRepo, ok := obj.(*fluxv1.HelmRepository) + if !ok { + return fmt.Errorf("expected HelmRepository object, got %T", obj) + } + helmRepo.Spec = *r.Config.Spec.ExternalDNSSource.Helm.DeepCopy() + return nil + } + kind = "HelmRepository" + for i := range existingHelm.Items { + obj := &existingHelm.Items[i] + if obj.GetName() != sourceName { + toBeDeleted = append(toBeDeleted, obj) + } + } + toBeDeleted = append(toBeDeleted, collections.ProjectSliceToSlice(existingGit.Items, func(obj fluxv1.GitRepository) client.Object { return &obj })...) + toBeDeleted = append(toBeDeleted, collections.ProjectSliceToSlice(existingOCI.Items, func(obj fluxv1.OCIRepository) client.Object { return &obj })...) + } else if r.Config.Spec.ExternalDNSSource.Git != nil { + fluxSource = &fluxv1.GitRepository{} + setSpec = func(obj client.Object) error { + gitRepo, ok := obj.(*fluxv1.GitRepository) + if !ok { + return fmt.Errorf("expected GitRepository object, got %T", obj) + } + gitRepo.Spec = *r.Config.Spec.ExternalDNSSource.Git.DeepCopy() + return nil + } + kind = "GitRepository" + for i := range existingGit.Items { + obj := &existingGit.Items[i] + if obj.GetName() != sourceName { + toBeDeleted = append(toBeDeleted, obj) + } + } + toBeDeleted = append(toBeDeleted, collections.ProjectSliceToSlice(existingHelm.Items, func(obj fluxv1.HelmRepository) client.Object { return &obj })...) + toBeDeleted = append(toBeDeleted, collections.ProjectSliceToSlice(existingOCI.Items, func(obj fluxv1.OCIRepository) client.Object { return &obj })...) + } else if r.Config.Spec.ExternalDNSSource.OCI != nil { + fluxSource = &fluxv1.OCIRepository{} + setSpec = func(obj client.Object) error { + ociRepo, ok := obj.(*fluxv1.OCIRepository) + if !ok { + return fmt.Errorf("expected OCIRepository object, got %T", obj) + } + ociRepo.Spec = *r.Config.Spec.ExternalDNSSource.OCI.DeepCopy() + return nil + } + kind = "OCIRepository" + for i := range existingOCI.Items { + obj := &existingOCI.Items[i] + if obj.GetName() != sourceName { + toBeDeleted = append(toBeDeleted, obj) + } + } + toBeDeleted = append(toBeDeleted, collections.ProjectSliceToSlice(existingHelm.Items, func(obj fluxv1.HelmRepository) client.Object { return &obj })...) + toBeDeleted = append(toBeDeleted, collections.ProjectSliceToSlice(existingGit.Items, func(obj fluxv1.GitRepository) client.Object { return &obj })...) + } else { + rr.ReconcileError = errutils.WithReason(fmt.Errorf("no flux source configured"), clusterconst.ReasonConfigurationProblem) + return rr + } + fluxSource.SetName(sourceName) + fluxSource.SetNamespace(c.Namespace) + log.Info("Creating or updating Flux source", "kind", kind, "sourceName", fluxSource.GetName(), "sourceNamespace", fluxSource.GetNamespace()) + if _, err := controllerutil.CreateOrUpdate(ctx, r.PlatformCluster.Client(), fluxSource, func() error { + fluxSource.SetLabels(maputils.Merge(fluxSource.GetLabels(), expectedLabels)) + return setSpec(fluxSource) + }); err != nil { + rr.ReconcileError = errutils.WithReason(fmt.Errorf("error creating or updating %s '%s/%s': %w", kind, fluxSource.GetNamespace(), fluxSource.GetName(), err), clusterconst.ReasonPlatformClusterInteractionProblem) + return rr + } + // delete obsolete sources + for _, obj := range toBeDeleted { + log.Info("Deleting obsolete Flux source", "kind", obj.GetObjectKind().GroupVersionKind().Kind, "name", obj.GetName(), "namespace", obj.GetNamespace()) + if err := r.PlatformCluster.Client().Delete(ctx, obj); err != nil { + if !apierrors.IsNotFound(err) { + rr.ReconcileError = errutils.WithReason(fmt.Errorf("error deleting obsolete %s '%s/%s': %w", obj.GetObjectKind().GroupVersionKind().Kind, obj.GetNamespace(), obj.GetName(), err), clusterconst.ReasonPlatformClusterInteractionProblem) + return rr + } + } + } + + return rr +} diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..73bcfa2 --- /dev/null +++ b/renovate.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "git-submodules": { + "enabled": true + }, + "minimumReleaseAge": "3 days", + "extends": [ + "config:recommended", + "config:best-practices", + "security:openssf-scorecard", + "helpers:pinGitHubActionDigests", + ":rebaseStalePrs" + ], + "postUpdateOptions": [ + "gomodTidy" + ], + "packageRules": [ + { + "matchManagers": [ + "gomod" + ], + "matchDepNames": [ + "go" + ], + "matchDepTypes": [ + "golang" + ], + "rangeStrategy": "bump" + }, + { + "matchPackageNames": [ + "github.com/openmcp-project/*" + ], + "description": "Update all components from openmcp-project immediately", + "rebaseWhen": "auto", + "minimumReleaseAge": "0 days", + "enabled": true + } + ] +} \ No newline at end of file