├── pkg ├── kind │ ├── testdata │ │ ├── empty.json │ │ ├── no-node.yaml │ │ ├── no-port-multi.yaml │ │ ├── label-only.yaml │ │ ├── no-port.yaml │ │ ├── port-only.yaml │ │ ├── expected │ │ │ ├── port-only.yaml │ │ │ ├── no-port-multi.yaml │ │ │ ├── label-only.yaml │ │ │ └── no-port.yaml │ │ └── custom-kind.yaml.tmpl │ ├── resources │ │ ├── hosts.toml.tmpl │ │ └── kind.yaml.tmpl │ ├── kindlogger.go │ ├── config.go │ └── config_test.go ├── controllers │ ├── doc.go │ ├── gitrepository │ │ ├── test │ │ │ └── resources │ │ │ │ └── file1 │ │ ├── git_repository.go │ │ └── github.go │ ├── custompackage │ │ └── test │ │ │ └── resources │ │ │ └── customPackages │ │ │ ├── helm │ │ │ ├── test │ │ │ │ ├── values.yaml │ │ │ │ ├── templates │ │ │ │ │ └── cm.yaml │ │ │ │ └── Chart.yaml │ │ │ └── app.yaml │ │ │ ├── testDir │ │ │ ├── app1 │ │ │ │ └── cm.yaml │ │ │ ├── app2 │ │ │ │ ├── one │ │ │ │ │ └── cm.yaml │ │ │ │ └── two │ │ │ │ │ └── cm.yaml │ │ │ ├── app.yaml │ │ │ └── app2.yaml │ │ │ ├── applicationSet │ │ │ ├── test1 │ │ │ │ └── apps │ │ │ │ │ ├── guestbook │ │ │ │ │ ├── kustomization.yaml │ │ │ │ │ ├── guestbook-ui-svc.yaml │ │ │ │ │ └── guestbook-ui-deployment.yaml │ │ │ │ │ └── guestbook2 │ │ │ │ │ ├── kustomization.yaml │ │ │ │ │ ├── guestbook-ui-svc.yaml │ │ │ │ │ └── guestbook-ui-deployment.yaml │ │ │ ├── no-generator-single-source.yaml │ │ │ ├── generator-single-source.yaml │ │ │ ├── generator-multi-sources.yaml │ │ │ └── generator-matrix.yaml │ │ │ └── testDir2 │ │ │ ├── exampleApp.yaml │ │ │ └── exampleApp2.yaml │ ├── localbuild │ │ ├── gitea_test.go │ │ ├── nginx.go │ │ ├── argo.go │ │ ├── resources │ │ │ └── argo │ │ │ │ └── ingress.yaml │ │ ├── installer.go │ │ └── gitea.go │ ├── run.go │ ├── crd.go │ └── resources │ │ └── idpbuilder.cnoe.io_custompackages.yaml ├── cmd │ ├── helpers │ │ ├── test-data │ │ │ ├── notyaml.yaml │ │ │ ├── notk8s.yaml │ │ │ └── valid.yaml │ │ ├── logger.go │ │ ├── validation.go │ │ └── validation_test.go │ ├── get │ │ ├── root.go │ │ └── packages.go │ ├── root.go │ ├── delete │ │ └── root.go │ └── version │ │ └── root.go ├── build │ ├── templates │ │ └── coredns │ │ │ ├── cm-coredns-custom.yaml │ │ │ ├── cm-coredns.yaml │ │ │ ├── cm-coredns-default.yaml.tmpl │ │ │ └── deployment-coredns.yaml │ ├── build_test.go │ ├── coredns.go │ └── tls_test.go ├── util │ ├── secret.go │ ├── gitea_test.go │ ├── idp.go │ ├── argocd.go │ ├── k8s.go │ ├── fs │ │ ├── fs_test.go │ │ └── fs.go │ ├── url_test.go │ ├── files │ │ └── files.go │ ├── git_repository_test.go │ ├── util_test.go │ ├── gitea.go │ └── url.go ├── k8s │ ├── test-resources │ │ └── input │ │ │ ├── argocd-cm.yaml │ │ │ ├── extra.yaml │ │ │ └── extra.yaml.tmpl │ ├── schema.go │ ├── client.go │ ├── util.go │ ├── deserialize_test.go │ ├── util_test.go │ └── deserialize.go ├── printer │ ├── printer.go │ ├── types │ │ └── internal_types.go │ ├── package.go │ ├── secret.go │ └── cluster.go └── resources │ └── localbuild │ └── application.go ├── tests └── e2e │ └── docker │ └── test-dockerfile ├── .gitignore ├── docs ├── images │ ├── git.png │ ├── my-app.png │ ├── idpbuilder.png │ └── my-app-repo.png ├── minimum-requirements.md └── private-registries.md ├── hack ├── argo-cd │ ├── argocd-rbac-dev.yaml │ ├── notifications-controller.yaml │ ├── argocd-cm.yaml │ ├── dex-server.yaml │ ├── argocd-redis.yaml │ ├── argocd-tls-certs-cm.yaml.tmpl │ ├── argocd-repo-server.yaml │ ├── argocd-application-controller.yaml │ ├── generate-manifests.sh │ ├── argocd-applicationset-controller.yaml │ ├── kustomization.yaml │ ├── argocd-server.yaml │ └── ingress.yaml.tmpl ├── embedded-resources.sh ├── ingress-nginx │ ├── cm-ingress-nginx-controller.yaml │ ├── generate-manifests.sh │ ├── service-ingress-nginx.yaml.tmpl │ ├── kustomization.yaml │ └── deployment-ingress-nginx.yaml ├── boilerplate.go.txt ├── install.sh └── gitea │ ├── generate-manifests.sh │ ├── values.yaml │ └── ingress.yaml.tmpl ├── .pre-commit-config.yaml ├── globals └── project.go ├── main.go ├── .devcontainer ├── devcontainer.json ├── postStartCommand.sh └── postCreateCommand.sh ├── .github ├── workflows │ ├── pr.yaml │ ├── slash-commands.yaml │ ├── codespell.yaml │ ├── code-scanner.yaml │ ├── e2e.yaml │ ├── release.yaml │ └── nightly.yaml └── CODEOWNERS ├── api └── v1alpha1 │ ├── groupversion_info.go │ ├── custom_package_types.go │ ├── gitrepository_types.go │ └── localbuild_types.go ├── .goreleaser.yaml ├── README.md └── Makefile /pkg/kind/testdata/empty.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pkg/controllers/doc.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | -------------------------------------------------------------------------------- /pkg/controllers/gitrepository/test/resources/file1: -------------------------------------------------------------------------------- 1 | hello -------------------------------------------------------------------------------- /pkg/cmd/helpers/test-data/notyaml.yaml: -------------------------------------------------------------------------------- 1 | not: 2 | a 3 | yaml 4 | -------------------------------------------------------------------------------- /pkg/cmd/helpers/test-data/notk8s.yaml: -------------------------------------------------------------------------------- 1 | name: me 2 | pluto: not a planet 3 | -------------------------------------------------------------------------------- /tests/e2e/docker/test-dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | COPY test-dockerfile . 3 | -------------------------------------------------------------------------------- /pkg/kind/testdata/no-node.yaml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | idpbuilder 2 | bin/* 3 | .DS_Store 4 | cover.out 5 | __debug* 6 | .vscode 7 | .idea -------------------------------------------------------------------------------- /docs/images/git.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnoe-io/idpbuilder/HEAD/docs/images/git.png -------------------------------------------------------------------------------- /docs/images/my-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnoe-io/idpbuilder/HEAD/docs/images/my-app.png -------------------------------------------------------------------------------- /pkg/controllers/custompackage/test/resources/customPackages/helm/test/values.yaml: -------------------------------------------------------------------------------- 1 | some: value 2 | -------------------------------------------------------------------------------- /docs/images/idpbuilder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnoe-io/idpbuilder/HEAD/docs/images/idpbuilder.png -------------------------------------------------------------------------------- /docs/images/my-app-repo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnoe-io/idpbuilder/HEAD/docs/images/my-app-repo.png -------------------------------------------------------------------------------- /pkg/controllers/custompackage/test/resources/customPackages/testDir/app1/cm.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: config 5 | data: 6 | test1: "one" 7 | -------------------------------------------------------------------------------- /pkg/controllers/custompackage/test/resources/customPackages/helm/test/templates/cm.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: config 5 | data: 6 | test1: "one" 7 | -------------------------------------------------------------------------------- /pkg/controllers/custompackage/test/resources/customPackages/testDir/app2/one/cm.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: one-config 5 | data: 6 | test1: "one" 7 | -------------------------------------------------------------------------------- /pkg/controllers/custompackage/test/resources/customPackages/testDir/app2/two/cm.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: two-config 5 | data: 6 | test1: "one" 7 | -------------------------------------------------------------------------------- /docs/minimum-requirements.md: -------------------------------------------------------------------------------- 1 | # Minimum Requirements 2 | 3 | The requirements for your cluster will depend on what it is running but we 4 | recommend a minimum of 4 CPU cores and 4GiB of RAM. 5 | -------------------------------------------------------------------------------- /hack/argo-cd/argocd-rbac-dev.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: argocd-rbac-cm 5 | data: 6 | policy.csv: | 7 | p, role:developer, applications, *, *, allow 8 | g, developer, role:developer 9 | -------------------------------------------------------------------------------- /pkg/build/templates/coredns/cm-coredns-custom.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: coredns-conf-custom 5 | namespace: kube-system 6 | data: 7 | custom.conf: | 8 | # insert custom rules here 9 | -------------------------------------------------------------------------------- /pkg/controllers/custompackage/test/resources/customPackages/applicationSet/test1/apps/guestbook/kustomization.yaml: -------------------------------------------------------------------------------- 1 | namePrefix: kustomize- 2 | 3 | resources: 4 | - guestbook-ui-deployment.yaml 5 | - guestbook-ui-svc.yaml 6 | apiVersion: kustomize.config.k8s.io/v1beta1 7 | kind: Kustomization 8 | -------------------------------------------------------------------------------- /pkg/controllers/custompackage/test/resources/customPackages/applicationSet/test1/apps/guestbook2/kustomization.yaml: -------------------------------------------------------------------------------- 1 | namePrefix: kustomize- 2 | 3 | resources: 4 | - guestbook-ui-deployment.yaml 5 | - guestbook-ui-svc.yaml 6 | apiVersion: kustomize.config.k8s.io/v1beta1 7 | kind: Kustomization 8 | -------------------------------------------------------------------------------- /pkg/controllers/custompackage/test/resources/customPackages/applicationSet/test1/apps/guestbook/guestbook-ui-svc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: guestbook-ui 5 | spec: 6 | ports: 7 | - port: 80 8 | targetPort: 80 9 | selector: 10 | app: guestbook-ui 11 | -------------------------------------------------------------------------------- /pkg/controllers/custompackage/test/resources/customPackages/applicationSet/test1/apps/guestbook2/guestbook-ui-svc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: guestbook-ui 5 | spec: 6 | ports: 7 | - port: 80 8 | targetPort: 80 9 | selector: 10 | app: guestbook-ui 11 | -------------------------------------------------------------------------------- /hack/embedded-resources.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DIRECTORIES='argo-cd gitea ingress-nginx' 4 | 5 | for dir in $DIRECTORIES; do 6 | ./hack/$dir/generate-manifests.sh; 7 | if [[ $? -ne 0 ]]; then 8 | echo "error running script: ./hack/$dir/generate-manifests.sh" 9 | exit 1 10 | fi 11 | done -------------------------------------------------------------------------------- /hack/argo-cd/notifications-controller.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: argocd-notifications-controller 5 | spec: 6 | replicas: 0 7 | template: 8 | spec: 9 | containers: 10 | - name: argocd-notifications-controller 11 | imagePullPolicy: IfNotPresent 12 | -------------------------------------------------------------------------------- /hack/ingress-nginx/cm-ingress-nginx-controller.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: ingress-nginx-controller 5 | namespace: ingress-nginx 6 | data: 7 | allow-snippet-annotations: "true" 8 | proxy-buffer-size: "32k" 9 | proxy-busy-buffers-size: "32k" 10 | use-forwarded-headers: "true" 11 | -------------------------------------------------------------------------------- /hack/argo-cd/argocd-cm.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: argocd-cm 5 | data: 6 | application.resourceTrackingMethod: annotation 7 | accounts.developer: apiKey, login 8 | timeout.reconciliation: 60s 9 | resource.exclusions: | 10 | - kinds: 11 | - ProviderConfigUsage 12 | apiGroups: 13 | - "*" 14 | -------------------------------------------------------------------------------- /hack/argo-cd/dex-server.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: argocd-dex-server 5 | spec: 6 | replicas: 0 7 | template: 8 | spec: 9 | containers: 10 | - name: dex 11 | imagePullPolicy: IfNotPresent 12 | initContainers: 13 | - name: copyutil 14 | imagePullPolicy: IfNotPresent 15 | -------------------------------------------------------------------------------- /pkg/util/secret.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "context" 5 | v1 "k8s.io/api/core/v1" 6 | "sigs.k8s.io/controller-runtime/pkg/client" 7 | ) 8 | 9 | func GetSecretByName(ctx context.Context, kubeClient client.Client, ns, name string) (v1.Secret, error) { 10 | s := v1.Secret{} 11 | return s, kubeClient.Get(ctx, client.ObjectKey{Name: name, Namespace: ns}, &s) 12 | } 13 | -------------------------------------------------------------------------------- /pkg/kind/testdata/no-port-multi.yaml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | nodes: 4 | - role: control-plane 5 | extraPortMappings: 6 | - containerPort: 31337 7 | hostPort: 31337 8 | - containerPort: 31340 9 | hostPort: 31340 10 | - containerPort: 31333 11 | hostPort: 31333 12 | - role: worker 13 | image: "abc" 14 | -------------------------------------------------------------------------------- /hack/argo-cd/argocd-redis.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app.kubernetes.io/component: redis 6 | app.kubernetes.io/name: argocd-redis 7 | app.kubernetes.io/part-of: argocd 8 | name: argocd-redis 9 | spec: 10 | template: 11 | spec: 12 | containers: 13 | - name: redis 14 | imagePullPolicy: IfNotPresent 15 | -------------------------------------------------------------------------------- /hack/argo-cd/argocd-tls-certs-cm.yaml.tmpl: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: argocd-tls-certs-cm 5 | labels: 6 | app.kubernetes.io/name: argocd-tls-certs-cm 7 | app.kubernetes.io/part-of: argocd 8 | data: 9 | 'gitea.{{.Host}}': | 10 | {{ .SelfSignedCert | indentNewLines 4 }} 11 | '{{.Host}}': | 12 | {{ .SelfSignedCert | indentNewLines 4 }} 13 | -------------------------------------------------------------------------------- /pkg/kind/resources/hosts.toml.tmpl: -------------------------------------------------------------------------------- 1 | {{ if .UsePathRouting -}} 2 | server = "https://{{ .Host }}:{{ .Port }}" 3 | 4 | [host."https://{{ .Host }}"] 5 | capabilities = ["pull", "resolve"] 6 | skip_verify = true 7 | {{ else -}} 8 | server = "https://gitea.{{ .Host }}:{{ .Port }}" 9 | 10 | [host."https://gitea.{{ .Host }}"] 11 | capabilities = ["pull", "resolve"] 12 | skip_verify = true 13 | {{ end -}} 14 | -------------------------------------------------------------------------------- /pkg/k8s/test-resources/input/argocd-cm.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | application.resourceTrackingMethod: annotation 4 | resource.exclusions: | 5 | - kinds: 6 | - ProviderConfigUsage 7 | apiGroups: 8 | - "*" 9 | kind: ConfigMap 10 | metadata: 11 | labels: 12 | app.kubernetes.io/name: argocd-cm 13 | app.kubernetes.io/part-of: argocd 14 | Test: Data 15 | name: argocd-cm 16 | -------------------------------------------------------------------------------- /hack/argo-cd/argocd-repo-server.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app.kubernetes.io/component: repo-server 6 | app.kubernetes.io/name: argocd-repo-server 7 | app.kubernetes.io/part-of: argocd 8 | name: argocd-repo-server 9 | spec: 10 | template: 11 | spec: 12 | containers: 13 | - name: argocd-repo-server 14 | imagePullPolicy: IfNotPresent 15 | -------------------------------------------------------------------------------- /pkg/kind/testdata/label-only.yaml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | nodes: 4 | - role: control-plane 5 | extraPortMappings: 6 | - containerPort: 31337 7 | hostPort: 31337 8 | - containerPort: 31340 9 | hostPort: 31340 10 | - containerPort: 31333 11 | hostPort: 31333 12 | - role: worker 13 | image: "abc" 14 | labels: 15 | ingress-ready: "true" 16 | -------------------------------------------------------------------------------- /pkg/kind/testdata/no-port.yaml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | nodes: 4 | - role: control-plane 5 | extraMounts: 6 | - containerPath: /var/lib/kubelet/config.json 7 | hostPath: ~/.docker/config.json 8 | extraPortMappings: 9 | - containerPort: 31337 10 | hostPort: 31337 11 | - containerPort: 31340 12 | hostPort: 31340 13 | - containerPort: 31333 14 | hostPort: 31333 15 | -------------------------------------------------------------------------------- /docs/private-registries.md: -------------------------------------------------------------------------------- 1 | # Private registry authentication 2 | 3 | idpbuilder can be configured to use private registry authentication from the 4 | host filesystem by using the `--registry-config` flag with the `create` command. 5 | By default this will look for a registry config file in the default 6 | podman and docker paths (see the help text for details). You can optionally 7 | specify a file by doing the following: 8 | `--registry-config=$HOME/path/to/auth.json` 9 | -------------------------------------------------------------------------------- /pkg/kind/testdata/port-only.yaml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | nodes: 4 | - role: control-plane 5 | extraPortMappings: 6 | - containerPort: 31337 7 | hostPort: 31337 8 | - containerPort: 31340 9 | hostPort: 31340 10 | - containerPort: 31333 11 | hostPort: 31333 12 | - role: worker 13 | extraPortMappings: 14 | - containerPort: 80 15 | hostPort: 80 16 | protocol: TCP 17 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # For more information, visit: https://pre-commit.com 2 | # To run locally: 3 | # 1. Install pre-commit: pip install pre-commit 4 | # 2. Run pre-commit checks on all files: pre-commit run --all-files 5 | 6 | repos: 7 | - repo: https://github.com/codespell-project/codespell 8 | rev: v2.2.6 9 | hooks: 10 | - id: codespell 11 | args: ["--skip=*.excalidraw,*.git,*.png,*.jpg,*.svg,go.sum,go.mod,./pkg/controllers/localbuild/resources"] 12 | -------------------------------------------------------------------------------- /hack/argo-cd/argocd-application-controller.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | labels: 5 | app.kubernetes.io/component: application-controller 6 | app.kubernetes.io/name: argocd-application-controller 7 | app.kubernetes.io/part-of: argocd 8 | name: argocd-application-controller 9 | spec: 10 | template: 11 | spec: 12 | containers: 13 | - name: argocd-application-controller 14 | imagePullPolicy: IfNotPresent 15 | -------------------------------------------------------------------------------- /hack/argo-cd/generate-manifests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | INSTALL_YAML="pkg/controllers/localbuild/resources/argo/install.yaml" 4 | INGRESS_YAML="pkg/controllers/localbuild/resources/argo/ingress.yaml" 5 | 6 | echo "# UCP ARGO INSTALL RESOURCES" > ${INSTALL_YAML} 7 | echo "# This file is auto-generated with 'hack/argo-cd/generate-manifests.sh'" >> ${INSTALL_YAML} 8 | kustomize build ./hack/argo-cd/ >> ${INSTALL_YAML} 9 | 10 | cat ./hack/argo-cd/ingress.yaml.tmpl > ${INGRESS_YAML} 11 | -------------------------------------------------------------------------------- /hack/argo-cd/argocd-applicationset-controller.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app.kubernetes.io/component: applicationset-controller 6 | app.kubernetes.io/name: argocd-applicationset-controller 7 | app.kubernetes.io/part-of: argocd 8 | name: argocd-applicationset-controller 9 | spec: 10 | template: 11 | spec: 12 | containers: 13 | - name: argocd-applicationset-controller 14 | imagePullPolicy: IfNotPresent -------------------------------------------------------------------------------- /hack/ingress-nginx/generate-manifests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | INSTALL_YAML="pkg/controllers/localbuild/resources/nginx/k8s/ingress-nginx.yaml" 4 | NGINX_DIR="./hack/ingress-nginx" 5 | 6 | 7 | echo "# INGRESS-NGINX INSTALL RESOURCES" > ${INSTALL_YAML} 8 | echo "# This file is auto-generated with 'hack/ingress-nginx/generate-manifests.sh'" >> ${INSTALL_YAML} 9 | kustomize build ${NGINX_DIR} >> ${INSTALL_YAML} 10 | 11 | cat ${NGINX_DIR}/service-ingress-nginx.yaml.tmpl >> ${INSTALL_YAML} 12 | -------------------------------------------------------------------------------- /pkg/controllers/custompackage/test/resources/customPackages/testDir/app.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: Application 3 | metadata: 4 | name: my-app 5 | namespace: argocd 6 | spec: 7 | destination: 8 | namespace: my-app 9 | server: "https://kubernetes.default.svc" 10 | source: 11 | repoURL: cnoe://app1 12 | targetRevision: HEAD 13 | path: "." 14 | project: default 15 | syncPolicy: 16 | automated: 17 | selfHeal: true 18 | syncOptions: 19 | - CreateNamespace=true 20 | -------------------------------------------------------------------------------- /pkg/controllers/custompackage/test/resources/customPackages/testDir2/exampleApp.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: Application 3 | metadata: 4 | name: guestbook 5 | namespace: argocd 6 | spec: 7 | project: default 8 | source: 9 | repoURL: https://github.com/argoproj/argocd-example-apps.git 10 | targetRevision: HEAD 11 | path: guestbook 12 | destination: 13 | server: https://kubernetes.default.svc 14 | namespace: guestbook 15 | syncPolicy: 16 | syncOptions: 17 | - CreateNamespace=true 18 | -------------------------------------------------------------------------------- /pkg/controllers/custompackage/test/resources/customPackages/testDir2/exampleApp2.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: Application 3 | metadata: 4 | name: guestbook2 5 | namespace: argocd 6 | spec: 7 | project: default 8 | source: 9 | repoURL: https://github.com/argoproj/argocd-example-apps.git 10 | targetRevision: HEAD 11 | path: guestbook 12 | destination: 13 | server: https://kubernetes.default.svc 14 | namespace: guestbook2 15 | syncPolicy: 16 | syncOptions: 17 | - CreateNamespace=true 18 | -------------------------------------------------------------------------------- /pkg/kind/testdata/expected/port-only.yaml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | networking: {} 4 | nodes: 5 | - role: control-plane 6 | extraPortMappings: 7 | - containerPort: 31337 8 | hostPort: 31337 9 | - containerPort: 31340 10 | hostPort: 31340 11 | - containerPort: 31333 12 | hostPort: 31333 13 | - role: worker 14 | labels: 15 | ingress-ready: "true" 16 | extraPortMappings: 17 | - containerPort: 80 18 | hostPort: 80 19 | protocol: TCP 20 | -------------------------------------------------------------------------------- /pkg/kind/testdata/expected/no-port-multi.yaml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | networking: {} 4 | nodes: 5 | - role: control-plane 6 | labels: 7 | ingress-ready: "true" 8 | extraPortMappings: 9 | - containerPort: 31337 10 | hostPort: 31337 11 | - containerPort: 31340 12 | hostPort: 31340 13 | - containerPort: 31333 14 | hostPort: 31333 15 | - containerPort: 443 16 | hostPort: 8443 17 | protocol: TCP 18 | - role: worker 19 | image: "abc" 20 | -------------------------------------------------------------------------------- /pkg/kind/testdata/expected/label-only.yaml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | networking: {} 4 | nodes: 5 | - role: control-plane 6 | extraPortMappings: 7 | - containerPort: 31337 8 | hostPort: 31337 9 | - containerPort: 31340 10 | hostPort: 31340 11 | - containerPort: 31333 12 | hostPort: 31333 13 | - role: worker 14 | image: "abc" 15 | labels: 16 | ingress-ready: "true" 17 | extraPortMappings: 18 | - containerPort: 443 19 | hostPort: 8443 20 | protocol: TCP 21 | 22 | -------------------------------------------------------------------------------- /globals/project.go: -------------------------------------------------------------------------------- 1 | package globals 2 | 3 | import "fmt" 4 | 5 | const ( 6 | ProjectName string = "idpbuilder" 7 | 8 | NginxNamespace string = "ingress-nginx" 9 | ArgoCDNamespace string = "argocd" 10 | 11 | SelfSignedCertSecretName = "idpbuilder-cert" 12 | SelfSignedCertCMName = "idpbuilder-cert" 13 | SelfSignedCertCMKeyName = "ca.crt" 14 | DefaultSANWildcard = "*.cnoe.localtest.me" 15 | DefaultHostName = "cnoe.localtest.me" 16 | ) 17 | 18 | func GetProjectNamespace(name string) string { 19 | return fmt.Sprintf("%s-%s", ProjectName, name) 20 | } 21 | -------------------------------------------------------------------------------- /pkg/controllers/custompackage/test/resources/customPackages/applicationSet/test1/apps/guestbook/guestbook-ui-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: guestbook-ui 5 | spec: 6 | replicas: 1 7 | revisionHistoryLimit: 3 8 | selector: 9 | matchLabels: 10 | app: guestbook-ui 11 | template: 12 | metadata: 13 | labels: 14 | app: guestbook-ui 15 | spec: 16 | containers: 17 | - image: gcr.io/heptio-images/ks-guestbook-demo:0.2 18 | name: guestbook-ui 19 | ports: 20 | - containerPort: 80 21 | -------------------------------------------------------------------------------- /pkg/controllers/custompackage/test/resources/customPackages/applicationSet/test1/apps/guestbook2/guestbook-ui-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: guestbook-ui 5 | spec: 6 | replicas: 1 7 | revisionHistoryLimit: 3 8 | selector: 9 | matchLabels: 10 | app: guestbook-ui 11 | template: 12 | metadata: 13 | labels: 14 | app: guestbook-ui 15 | spec: 16 | containers: 17 | - image: gcr.io/heptio-images/ks-guestbook-demo:0.2 18 | name: guestbook-ui 19 | ports: 20 | - containerPort: 80 21 | -------------------------------------------------------------------------------- /pkg/controllers/localbuild/gitea_test.go: -------------------------------------------------------------------------------- 1 | package localbuild 2 | 3 | import ( 4 | "context" 5 | "github.com/cnoe-io/idpbuilder/pkg/util" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestGetGiteaToken(t *testing.T) { 15 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 16 | time.Sleep(time.Second * 35) 17 | })) 18 | defer ts.Close() 19 | ctx := context.Background() 20 | _, err := util.GetGiteaToken(ctx, ts.URL, "", "") 21 | require.Error(t, err) 22 | } 23 | -------------------------------------------------------------------------------- /hack/argo-cd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - https://raw.githubusercontent.com/argoproj/argo-cd/v3.1.7/manifests/install.yaml 5 | 6 | patches: 7 | - path: dex-server.yaml 8 | - path: notifications-controller.yaml 9 | - path: argocd-cm.yaml 10 | - path: argocd-server.yaml 11 | - path: argocd-application-controller.yaml 12 | - path: argocd-applicationset-controller.yaml 13 | - path: argocd-repo-server.yaml 14 | - path: argocd-redis.yaml 15 | - path: argocd-tls-certs-cm.yaml.tmpl 16 | - path: argocd-rbac-dev.yaml 17 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | 10 | "github.com/cnoe-io/idpbuilder/pkg/cmd" 11 | ) 12 | 13 | func main() { 14 | interrupted := make(chan os.Signal, 1) 15 | defer close(interrupted) 16 | signal.Notify(interrupted, os.Interrupt, syscall.SIGTERM) 17 | defer signal.Stop(interrupted) 18 | 19 | ctx, cancel := context.WithCancelCause(context.Background()) 20 | 21 | go func() { 22 | select { 23 | case <-interrupted: 24 | cancel(fmt.Errorf("command interrupted")) 25 | } 26 | }() 27 | 28 | cmd.Execute(ctx) 29 | } 30 | -------------------------------------------------------------------------------- /pkg/kind/testdata/expected/no-port.yaml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | networking: {} 4 | nodes: 5 | - role: control-plane 6 | labels: 7 | ingress-ready: "true" 8 | extraMounts: 9 | - containerPath: /var/lib/kubelet/config.json 10 | hostPath: ~/.docker/config.json 11 | extraPortMappings: 12 | - containerPort: 31337 13 | hostPort: 31337 14 | - containerPort: 31340 15 | hostPort: 31340 16 | - containerPort: 31333 17 | hostPort: 31333 18 | - containerPort: 443 19 | hostPort: 8443 20 | protocol: TCP 21 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ -------------------------------------------------------------------------------- /pkg/controllers/custompackage/test/resources/customPackages/testDir/app2.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: Application 3 | metadata: 4 | name: my-app2 5 | namespace: argocd 6 | spec: 7 | destination: 8 | namespace: my-app2 9 | server: "https://kubernetes.default.svc" 10 | sources: 11 | - repoURL: cnoe://app2 12 | targetRevision: HEAD 13 | path: "one" 14 | - repoURL: cnoe://app2 15 | targetRevision: HEAD 16 | path: "two" 17 | project: default 18 | syncPolicy: 19 | automated: 20 | selfHeal: true 21 | syncOptions: 22 | - CreateNamespace=true 23 | -------------------------------------------------------------------------------- /pkg/util/gitea_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/cnoe-io/idpbuilder/api/v1alpha1" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestGiteaBaseUrl(t *testing.T) { 11 | c := v1alpha1.BuildCustomizationSpec{ 12 | Protocol: "http", 13 | Port: "8080", 14 | Host: "cnoe.localtest.me", 15 | UsePathRouting: false, 16 | } 17 | 18 | s := GiteaBaseUrl(c) 19 | assert.Equal(t, "http://gitea.cnoe.localtest.me:8080", s) 20 | c.UsePathRouting = true 21 | s = GiteaBaseUrl(c) 22 | assert.Equal(t, "http://cnoe.localtest.me:8080/gitea", s) 23 | } 24 | -------------------------------------------------------------------------------- /pkg/controllers/custompackage/test/resources/customPackages/helm/app.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: Application 3 | metadata: 4 | name: my-app-helm 5 | namespace: argocd 6 | spec: 7 | destination: 8 | namespace: my-app-helm 9 | server: "https://kubernetes.default.svc" 10 | source: 11 | repoURL: cnoe://test 12 | targetRevision: HEAD 13 | path: "." 14 | helm: 15 | valuesObject: 16 | repoURLGit: cnoe://test 17 | nested: 18 | repoURLGit: cnoe://test 19 | project: default 20 | syncPolicy: 21 | automated: 22 | selfHeal: true 23 | syncOptions: 24 | - CreateNamespace=true 25 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "mcr.microsoft.com/vscode/devcontainers/base:ubuntu", 3 | "features": { 4 | "ghcr.io/devcontainers/features/go:1": { 5 | "version": "1.22" 6 | }, 7 | "ghcr.io/devcontainers/features/docker-in-docker:2": {} 8 | }, 9 | "postCreateCommand": ".devcontainer/postCreateCommand.sh", 10 | "postStartCommand": ".devcontainer/postStartCommand.sh", 11 | "workspaceFolder": "/home/vscode/idpbuilder", 12 | "workspaceMount": "source=${localWorkspaceFolder},target=/home/vscode/idpbuilder,type=bind", 13 | "hostRequirements": { 14 | "cpus": 4 15 | }, 16 | "remoteEnv": { 17 | "PATH": "${containerEnv:PATH}:/home/vscode/idpbuilder" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pkg/controllers/custompackage/test/resources/customPackages/applicationSet/no-generator-single-source.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: ApplicationSet 3 | metadata: 4 | name: no-generator-single-source 5 | namespace: argocd 6 | spec: 7 | generators: 8 | - clusters: { } 9 | template: 10 | metadata: 11 | name: '{{path.basename}}' 12 | spec: 13 | project: default 14 | source: 15 | repoURL: cnoe://test1 16 | targetRevision: HEAD 17 | path: '{{path}}' 18 | destination: 19 | server: https://kubernetes.default.svc 20 | namespace: '{{path.basename}}' 21 | syncPolicy: 22 | syncOptions: 23 | - CreateNamespace=true 24 | -------------------------------------------------------------------------------- /pkg/build/templates/coredns/cm-coredns.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: coredns 5 | namespace: kube-system 6 | data: 7 | Corefile: | 8 | .:53 { 9 | errors 10 | health { 11 | lameduck 5s 12 | } 13 | ready 14 | 15 | import ../coredns-configs/*.conf 16 | 17 | kubernetes cluster.local in-addr.arpa ip6.arpa { 18 | pods insecure 19 | fallthrough in-addr.arpa ip6.arpa 20 | ttl 30 21 | } 22 | prometheus :9153 23 | forward . /etc/resolv.conf { 24 | max_concurrent 1000 25 | } 26 | cache 30 27 | loop 28 | reload 29 | loadbalance 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/pr.yaml: -------------------------------------------------------------------------------- 1 | name: PR 2 | 3 | on: 4 | pull_request: 5 | types: [opened, ready_for_review, synchronize] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-22.04 10 | steps: 11 | - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 12 | - name: Setup Go 13 | uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 14 | with: 15 | go-version: '1.22' 16 | - name: Run tests 17 | run: | 18 | make build 19 | if [[ -n $(git status --porcelain) ]]; then 20 | echo "git is in dirty state"; 21 | git status --porcelain=2 --branch 22 | exit 1 23 | fi 24 | make test 25 | -------------------------------------------------------------------------------- /.github/workflows/slash-commands.yaml: -------------------------------------------------------------------------------- 1 | name: slash-commands 2 | 3 | on: 4 | issue_comment: 5 | types: [created] 6 | 7 | jobs: 8 | slash_command_dispatch: 9 | runs-on: ubuntu-22.04 10 | steps: 11 | - name: Generate a token 12 | id: generate-token 13 | uses: actions/create-github-app-token@v1 14 | with: 15 | app-id: ${{ vars.CNOE_GH_WORKFLOW_TOKEN_APP_ID }} 16 | private-key: ${{ secrets.CNOE_GH_WORKFLOW_TOKEN_PRIVATE_KEY }} 17 | - name: Slash Command Dispatch 18 | uses: peter-evans/slash-command-dispatch@v4 19 | with: 20 | token: ${{ steps.generate-token.outputs.token }} 21 | commands: | 22 | e2e 23 | permission: write 24 | issue-type: pull-request 25 | -------------------------------------------------------------------------------- /hack/argo-cd/argocd-server.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app.kubernetes.io/component: server 6 | app.kubernetes.io/name: argocd-server 7 | app.kubernetes.io/part-of: argocd 8 | name: argocd-server 9 | spec: 10 | selector: 11 | matchLabels: 12 | app.kubernetes.io/name: argocd-server 13 | template: 14 | metadata: 15 | labels: 16 | app.kubernetes.io/name: argocd-server 17 | spec: 18 | containers: 19 | - args: 20 | - /usr/local/bin/argocd-server 21 | - "{{if .UsePathRouting}}" 22 | - --insecure 23 | - --basehref 24 | - /argocd 25 | - "{{end}}" 26 | name: argocd-server 27 | imagePullPolicy: IfNotPresent 28 | -------------------------------------------------------------------------------- /pkg/k8s/test-resources/input/extra.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | stringData: 3 | test: data 4 | kind: Secret 5 | metadata: 6 | labels: 7 | Test: Data 8 | name: secret-one 9 | --- 10 | apiVersion: v1 11 | stringData: 12 | test: data 13 | kind: Secret 14 | metadata: 15 | labels: 16 | Test: Data 17 | name: secret-two 18 | --- 19 | apiVersion: v1 20 | kind: Service 21 | metadata: 22 | labels: 23 | test: test 24 | name: ingress-nginx-controller-admission 25 | namespace: ingress-nginx 26 | spec: 27 | ports: 28 | - appProtocol: https 29 | name: https-webhook 30 | port: 443 31 | targetPort: webhook 32 | selector: 33 | app.kubernetes.io/component: controller 34 | app.kubernetes.io/instance: ingress-nginx 35 | app.kubernetes.io/name: ingress-nginx 36 | type: ClusterIP 37 | -------------------------------------------------------------------------------- /pkg/printer/printer.go: -------------------------------------------------------------------------------- 1 | package printer 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | "k8s.io/cli-runtime/pkg/printers" 8 | "sigs.k8s.io/yaml" 9 | ) 10 | 11 | func PrintDataAsTable(table metav1.Table, outWriter io.Writer) error { 12 | printer := printers.NewTablePrinter(printers.PrintOptions{}) 13 | return printer.PrintObj(&table, outWriter) 14 | } 15 | 16 | func PrintDataAsJson(data any, outWriter io.Writer) error { 17 | enc := json.NewEncoder(outWriter) 18 | enc.SetEscapeHTML(false) 19 | enc.SetIndent("", " ") 20 | return enc.Encode(data) 21 | } 22 | 23 | func PrintDataAsYaml(data any, outWriter io.Writer) error { 24 | b, err := yaml.Marshal(data) 25 | if err != nil { 26 | return err 27 | } 28 | _, err = outWriter.Write(b) 29 | return err 30 | } 31 | -------------------------------------------------------------------------------- /pkg/controllers/custompackage/test/resources/customPackages/applicationSet/generator-single-source.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: ApplicationSet 3 | metadata: 4 | name: generator-single-source 5 | namespace: argocd 6 | spec: 7 | generators: 8 | - git: 9 | repoURL: cnoe://test1 10 | revision: HEAD 11 | directories: 12 | - path: apps/* 13 | template: 14 | metadata: 15 | name: '{{path.basename}}' 16 | spec: 17 | project: default 18 | source: 19 | repoURL: cnoe://test1 20 | targetRevision: HEAD 21 | path: '{{path}}' 22 | destination: 23 | server: https://kubernetes.default.svc 24 | namespace: '{{path.basename}}' 25 | syncPolicy: 26 | syncOptions: 27 | - CreateNamespace=true 28 | -------------------------------------------------------------------------------- /pkg/controllers/custompackage/test/resources/customPackages/applicationSet/generator-multi-sources.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: ApplicationSet 3 | metadata: 4 | name: generator-multi-sources 5 | namespace: argocd 6 | spec: 7 | generators: 8 | - git: 9 | repoURL: cnoe://test1 10 | revision: HEAD 11 | directories: 12 | - path: apps/* 13 | template: 14 | metadata: 15 | name: '{{path.basename}}' 16 | spec: 17 | project: default 18 | sources: 19 | - repoURL: cnoe://test1 20 | targetRevision: HEAD 21 | path: '{{path}}' 22 | destination: 23 | server: https://kubernetes.default.svc 24 | namespace: '{{path.basename}}' 25 | syncPolicy: 26 | syncOptions: 27 | - CreateNamespace=true 28 | -------------------------------------------------------------------------------- /pkg/build/templates/coredns/cm-coredns-default.yaml.tmpl: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: coredns-conf-default 5 | namespace: kube-system 6 | data: 7 | default.conf: | 8 | # Goal: Rewrite rules for in-cluster access to a service: gitea, argocd, etc using the same FQDN as for external access 9 | 10 | # subdomain names e.g. gitea.cnoe.localtest.me resolves to the IP address of the kubernetes ingress service and then will become ingress-nginx-controller.ingress-nginx.svc.cluster.local 11 | rewrite stop { 12 | name regex (.*).{{ .Host }} ingress-nginx-controller.ingress-nginx.svc.cluster.local answer auto 13 | } 14 | 15 | # host name resolves to the IP address of the kubernetes ingress service 16 | rewrite name exact {{ .Host }} ingress-nginx-controller.ingress-nginx.svc.cluster.local 17 | -------------------------------------------------------------------------------- /.github/workflows/codespell.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Codespell 3 | 4 | on: 5 | push: 6 | branches: [main] 7 | pull_request: 8 | branches: [main] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | codespell: 15 | name: Check for spelling errors 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v3 21 | - name: Codespell 22 | uses: codespell-project/actions-codespell@v2 23 | with: 24 | check_filenames: true 25 | # When using this Action in other repos, the --skip option below can be removed 26 | skip: "*.excalidraw,*.git,*.png,*.jpg,*.svg,go.mod,go.sum,./pkg/controllers/localbuild/resources" 27 | continue-on-error: true # The PR checks will not fail, but the possible spelling issues will still be reported for review and correction -------------------------------------------------------------------------------- /hack/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -o pipefail 4 | # get the latest stable release by look for tag name pattern like 'v*.*.*'. For example, v1.1.1 5 | # GitHub API returns releases in chronological order so we take the first matching tag name. 6 | version=$(curl -s https://api.github.com/repos/cnoe-io/idpbuilder/releases | grep tag_name | grep -o -e '"v[0-9].[0-9].[0-9]"' | head -n1 | sed 's/"//g') 7 | 8 | echo "Downloading idpbuilder version ${version}" 9 | curl -L --progress-bar -o ./idpbuilder.tar.gz "https://github.com/cnoe-io/idpbuilder/releases/download/${version}/idpbuilder-$(uname | awk '{print tolower($0)}')-$(uname -m | sed -e 's/x86_64/amd64/' -e 's/aarch64/arm64/').tar.gz" 10 | tar xzf idpbuilder.tar.gz 11 | 12 | echo "Moving idpbuilder binary to /usr/local/bin" 13 | sudo mv ./idpbuilder /usr/local/bin/ 14 | idpbuilder version 15 | echo "Successfully installed idpbuilder" 16 | -------------------------------------------------------------------------------- /api/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | // +kubebuilder:object:generate=true 2 | // +groupName=idpbuilder.cnoe.io 3 | package v1alpha1 4 | 5 | import ( 6 | "k8s.io/apimachinery/pkg/runtime/schema" 7 | "sigs.k8s.io/controller-runtime/pkg/scheme" 8 | ) 9 | 10 | var ( 11 | // GroupVersion is group version used to register these objects 12 | GroupVersion = schema.GroupVersion{Group: "idpbuilder.cnoe.io", Version: "v1alpha1"} 13 | 14 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 15 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 16 | 17 | // AddToScheme adds the types in this group-version to the given scheme. 18 | AddToScheme = SchemeBuilder.AddToScheme 19 | ) 20 | 21 | func init() { 22 | SchemeBuilder.Register(&Localbuild{}, &LocalbuildList{}) 23 | SchemeBuilder.Register(&GitRepository{}, &GitRepositoryList{}) 24 | SchemeBuilder.Register(&CustomPackage{}, &CustomPackageList{}) 25 | } 26 | -------------------------------------------------------------------------------- /pkg/cmd/helpers/test-data/valid.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: nginx-deployment 5 | labels: 6 | app: nginx 7 | spec: 8 | replicas: 3 9 | selector: 10 | matchLabels: 11 | app: nginx 12 | template: 13 | metadata: 14 | labels: 15 | app: nginx 16 | spec: 17 | containers: 18 | - name: nginx 19 | image: nginx:1.14.2 20 | ports: 21 | - containerPort: 80 22 | --- 23 | apiVersion: v1 24 | kind: PersistentVolume 25 | metadata: 26 | name: my-pv 27 | spec: 28 | capacity: 29 | storage: 1Gi 30 | accessModes: 31 | - ReadWriteOnce 32 | storageClassName: standard 33 | hostPath: 34 | path: /mnt/data 35 | --- 36 | apiVersion: v1 37 | kind: Service 38 | metadata: 39 | name: nginx-service 40 | spec: 41 | selector: 42 | app: nginx 43 | ports: 44 | - protocol: TCP 45 | port: 80 46 | targetPort: 80 47 | 48 | -------------------------------------------------------------------------------- /.github/workflows/code-scanner.yaml: -------------------------------------------------------------------------------- 1 | name: code-scanner 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [ "main" ] 9 | types: [opened, ready_for_review, synchronize] 10 | 11 | permissions: 12 | contents: write 13 | security-events: write # for github/codeql-action/upload-sarif to upload SARIF results 14 | 15 | jobs: 16 | code-analysis: 17 | runs-on: ubuntu-22.04 18 | steps: 19 | - uses: actions/checkout@v4 20 | - run: ls -al 21 | - name: Scan current project 22 | id: scan 23 | uses: anchore/scan-action@v3 24 | with: 25 | fail-build: false 26 | path: "." 27 | 28 | - name: Upload scan results to GitHub Security tab 29 | uses: github/codeql-action/upload-sarif@v3 30 | if: always() 31 | with: 32 | sarif_file: ${{ steps.scan.outputs.sarif }} 33 | -------------------------------------------------------------------------------- /pkg/cmd/get/root.go: -------------------------------------------------------------------------------- 1 | package get 2 | 3 | import ( 4 | "fmt" 5 | "github.com/cnoe-io/idpbuilder/pkg/util" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var GetCmd = &cobra.Command{ 10 | Use: "get", 11 | Short: "get information from the cluster", 12 | Long: ``, 13 | RunE: exportE, 14 | } 15 | 16 | var ( 17 | packages []string 18 | outputFormat string 19 | ) 20 | 21 | func init() { 22 | GetCmd.AddCommand(ClustersCmd) 23 | GetCmd.AddCommand(SecretsCmd) 24 | GetCmd.AddCommand(PackagesCmd) 25 | GetCmd.PersistentFlags().StringSliceVarP(&packages, "packages", "p", []string{}, "names of packages.") 26 | GetCmd.PersistentFlags().StringVarP(&outputFormat, "output", "o", "table", "Output format: table (default if not specified), json or yaml.") 27 | GetCmd.PersistentFlags().StringVarP(&util.KubeConfigPath, "kubeconfig", "", "", "kube config file Path.") 28 | } 29 | 30 | func exportE(cmd *cobra.Command, args []string) error { 31 | return fmt.Errorf("specify subcommand") 32 | } 33 | -------------------------------------------------------------------------------- /pkg/k8s/schema.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | argov1alpha1 "github.com/cnoe-io/argocd-api/api/argo/application/v1alpha1" 5 | "github.com/cnoe-io/idpbuilder/api/v1alpha1" 6 | admissionregistrationv1 "k8s.io/api/admissionregistration/v1" 7 | appsv1 "k8s.io/api/apps/v1" 8 | batchv1 "k8s.io/api/batch/v1" 9 | corev1 "k8s.io/api/core/v1" 10 | networkingv1 "k8s.io/api/networking/v1" 11 | rbacv1 "k8s.io/api/rbac/v1" 12 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 13 | "k8s.io/apimachinery/pkg/runtime" 14 | ) 15 | 16 | func GetScheme() *runtime.Scheme { 17 | scheme := runtime.NewScheme() 18 | schemeBuilder := runtime.NewSchemeBuilder( 19 | admissionregistrationv1.AddToScheme, 20 | apiextensionsv1.AddToScheme, 21 | appsv1.AddToScheme, 22 | batchv1.AddToScheme, 23 | corev1.AddToScheme, 24 | networkingv1.AddToScheme, 25 | rbacv1.AddToScheme, 26 | argov1alpha1.AddToScheme, 27 | v1alpha1.AddToScheme, 28 | ) 29 | schemeBuilder.AddToScheme(scheme) 30 | return scheme 31 | } 32 | -------------------------------------------------------------------------------- /pkg/controllers/custompackage/test/resources/customPackages/applicationSet/generator-matrix.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: ApplicationSet 3 | metadata: 4 | name: generator-matrix 5 | namespace: argocd 6 | spec: 7 | goTemplate: true 8 | goTemplateOptions: 9 | - missingkey=error 10 | generators: 11 | - matrix: 12 | generators: 13 | - git: 14 | repoURL: "cnoe://test1" 15 | revision: HEAD 16 | files: 17 | - path: "**/config.yaml" 18 | template: 19 | metadata: 20 | name: "{{ .name }}" 21 | labels: 22 | environment: "{{ .environment }}" 23 | spec: 24 | project: default 25 | source: 26 | repoURL: "cnoe://test1" 27 | targetRevision: HEAD 28 | path: "{{ .manifestPath }}/manifests" 29 | destination: 30 | server: https://kubernetes.default.svc 31 | namespace: "{{ .namespace }}" 32 | syncPolicy: 33 | syncOptions: 34 | - CreateNamespace=true 35 | -------------------------------------------------------------------------------- /pkg/util/idp.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/cnoe-io/idpbuilder/api/v1alpha1" 7 | "sigs.k8s.io/controller-runtime/pkg/client" 8 | ) 9 | 10 | func GetConfig(ctx context.Context) (v1alpha1.BuildCustomizationSpec, error) { 11 | b := v1alpha1.BuildCustomizationSpec{} 12 | 13 | kubeConfig, err := GetKubeConfig() 14 | if err != nil { 15 | return b, fmt.Errorf("getting kube config: %w", err) 16 | } 17 | 18 | kubeClient, err := GetKubeClient(kubeConfig) 19 | if err != nil { 20 | return b, fmt.Errorf("getting kube client: %w", err) 21 | } 22 | 23 | list, err := getLocalBuild(ctx, kubeClient) 24 | if err != nil { 25 | return b, err 26 | } 27 | 28 | // TODO: We assume that only one LocalBuild exists ! 29 | return list.Items[0].Spec.BuildCustomization, nil 30 | } 31 | 32 | func getLocalBuild(ctx context.Context, kubeClient client.Client) (v1alpha1.LocalbuildList, error) { 33 | localBuildList := v1alpha1.LocalbuildList{} 34 | return localBuildList, kubeClient.List(ctx, &localBuildList) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/util/argocd.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "github.com/cnoe-io/idpbuilder/api/v1alpha1" 6 | corev1 "k8s.io/api/core/v1" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | ) 9 | 10 | const ( 11 | ArgocdInitialAdminSecretName = "argocd-initial-admin-secret" 12 | ArgocdAdminName = "admin" 13 | ArgocdNamespace = "argocd" 14 | ArgocdURLTempl = "%s://%s%s:%s%s" 15 | ) 16 | 17 | func ArgocdBaseUrl(config v1alpha1.BuildCustomizationSpec) string { 18 | if config.UsePathRouting { 19 | return fmt.Sprintf(ArgocdURLTempl, config.Protocol, "", config.Host, config.Port, "/argocd") 20 | } 21 | return fmt.Sprintf(ArgocdURLTempl, config.Protocol, "argocd.", config.Host, config.Port, "") 22 | } 23 | 24 | func ArgocdInitialAdminSecretObject() corev1.Secret { 25 | return corev1.Secret{ 26 | TypeMeta: metav1.TypeMeta{ 27 | Kind: "Secret", 28 | APIVersion: "v1", 29 | }, 30 | ObjectMeta: metav1.ObjectMeta{ 31 | Name: ArgocdInitialAdminSecretName, 32 | Namespace: ArgocdNamespace, 33 | }, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pkg/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/cnoe-io/idpbuilder/pkg/cmd/create" 9 | "github.com/cnoe-io/idpbuilder/pkg/cmd/delete" 10 | "github.com/cnoe-io/idpbuilder/pkg/cmd/get" 11 | "github.com/cnoe-io/idpbuilder/pkg/cmd/helpers" 12 | "github.com/cnoe-io/idpbuilder/pkg/cmd/version" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | var rootCmd = &cobra.Command{ 17 | Use: "idpbuilder", 18 | Short: "Manage reference IDPs", 19 | Long: "", 20 | } 21 | 22 | func init() { 23 | rootCmd.PersistentFlags().StringVarP(&helpers.LogLevel, "log-level", "l", "info", helpers.LogLevelMsg) 24 | rootCmd.PersistentFlags().BoolVar(&helpers.ColoredOutput, "color", false, helpers.ColoredOutputMsg) 25 | rootCmd.AddCommand(create.CreateCmd) 26 | rootCmd.AddCommand(get.GetCmd) 27 | rootCmd.AddCommand(delete.DeleteCmd) 28 | rootCmd.AddCommand(version.VersionCmd) 29 | } 30 | 31 | func Execute(ctx context.Context) { 32 | if err := rootCmd.ExecuteContext(ctx); err != nil { 33 | fmt.Fprintln(os.Stderr, err) 34 | os.Exit(1) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pkg/kind/resources/kind.yaml.tmpl: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | nodes: 4 | - role: control-plane 5 | image: "kindest/node:{{ .KubernetesVersion }}" 6 | labels: 7 | ingress-ready: "true" 8 | extraPortMappings: 9 | - containerPort: {{ if (eq .Protocol "http") -}} 80 {{- else -}} 443 {{- end }} 10 | hostPort: {{ .Port }} 11 | {{- if .StaticPassword }} 12 | listenAddress: "127.0.0.1" 13 | {{- end }} 14 | protocol: TCP 15 | - containerPort: 32222 16 | hostPort: 32222 17 | protocol: TCP 18 | {{- range .ExtraPortsMapping }} 19 | - containerPort: {{ .ContainerPort }} 20 | hostPort: {{ .HostPort }} 21 | protocol: TCP 22 | {{- end }} 23 | extraMounts: 24 | - containerPath: /etc/containerd/certs.d 25 | hostPath: {{ .RegistryCertsDir }} 26 | {{- if .RegistryConfig }} 27 | - containerPath: /var/lib/kubelet/config.json 28 | hostPath: {{ .RegistryConfig }} 29 | {{- end }} 30 | containerdConfigPatches: 31 | - |- 32 | [plugins."io.containerd.grpc.v1.cri".registry] 33 | config_path = "/etc/containerd/certs.d" 34 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | # 18 | # Any committer can add themselves to any of the path patterns 19 | # and will subsequently get requested as a reviewer for any PRs 20 | # that change matching files. 21 | 22 | /* @cnoe-io/idpbuilder-approvers 23 | /* @cnoe-io/idpbuilder-admin -------------------------------------------------------------------------------- /hack/ingress-nginx/service-ingress-nginx.yaml.tmpl: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | labels: 6 | app.kubernetes.io/component: controller 7 | app.kubernetes.io/instance: ingress-nginx 8 | app.kubernetes.io/name: ingress-nginx 9 | app.kubernetes.io/part-of: ingress-nginx 10 | app.kubernetes.io/version: 1.8.1 11 | name: ingress-nginx-controller 12 | namespace: ingress-nginx 13 | spec: 14 | ipFamilies: 15 | - IPv4 16 | ipFamilyPolicy: SingleStack 17 | ports: 18 | - appProtocol: {{ .Protocol }} 19 | name: {{ .Protocol }}-{{ .Port }} 20 | port: {{ .Port }} 21 | protocol: TCP 22 | targetPort: {{ .Protocol }} 23 | - appProtocol: http 24 | name: http 25 | port: 80 26 | protocol: TCP 27 | targetPort: http 28 | - appProtocol: https 29 | name: https 30 | port: 443 31 | protocol: TCP 32 | targetPort: https 33 | selector: 34 | app.kubernetes.io/component: controller 35 | app.kubernetes.io/instance: ingress-nginx 36 | app.kubernetes.io/name: ingress-nginx 37 | type: NodePort 38 | -------------------------------------------------------------------------------- /.devcontainer/postStartCommand.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Import GPG key if both parts are available 4 | if [ -n "$GPG_SECRET_KEY_PART1" ] && [ -n "$GPG_SECRET_KEY_PART2" ]; then 5 | echo "Importing GPG key..." 6 | echo "$GPG_SECRET_KEY_PART1$GPG_SECRET_KEY_PART2" | tr -d "'" | base64 -d | gunzip | gpg --batch --yes --no-tty --import 7 | if [ $? -eq 0 ]; then 8 | echo "GPG key imported successfully" 9 | 10 | # Automatically configure Git to use the imported key for signing 11 | echo "Configuring Git to use the imported GPG key..." 12 | GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep -E "^sec" | head -1 | awk '{print $2}' | cut -d'/' -f2) 13 | 14 | if [ -n "$GPG_KEY_ID" ]; then 15 | git config --global user.signingkey "$GPG_KEY_ID" 16 | echo "Git configured to use GPG key: $GPG_KEY_ID" 17 | else 18 | echo "Warning: Could not detect GPG key ID for Git configuration" 19 | fi 20 | else 21 | echo "Failed to import GPG key" 22 | fi 23 | else 24 | echo "GPG key parts not found, skipping GPG import" 25 | fi -------------------------------------------------------------------------------- /pkg/k8s/test-resources/input/extra.yaml.tmpl: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | stringData: 3 | test: data 4 | kind: Secret 5 | metadata: 6 | labels: 7 | Test: Data 8 | name: secret-one 9 | --- 10 | apiVersion: v1 11 | stringData: 12 | test: data 13 | kind: Secret 14 | metadata: 15 | labels: 16 | Test: Data 17 | name: secret-two 18 | --- 19 | apiVersion: v1 20 | kind: Service 21 | metadata: 22 | labels: 23 | test: data 24 | name: ingress-nginx-controller 25 | namespace: ingress-nginx 26 | spec: 27 | ipFamilies: 28 | - IPv4 29 | ipFamilyPolicy: SingleStack 30 | ports: 31 | - appProtocol: {{ .Protocol }} 32 | name: {{ .Protocol }}-{{ .Port }} 33 | port: {{ .Port }} 34 | protocol: TCP 35 | targetPort: {{ .Protocol }} 36 | - appProtocol: http 37 | name: http 38 | port: 80 39 | protocol: TCP 40 | targetPort: http 41 | - appProtocol: https 42 | name: https 43 | port: 443 44 | protocol: TCP 45 | targetPort: https 46 | selector: 47 | app.kubernetes.io/component: controller 48 | app.kubernetes.io/instance: ingress-nginx 49 | app.kubernetes.io/name: ingress-nginx 50 | type: NodePort 51 | -------------------------------------------------------------------------------- /pkg/kind/testdata/custom-kind.yaml.tmpl: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | nodes: 4 | - role: control-plane 5 | image: "kindest/node:{{ .KubernetesVersion }}" 6 | labels: 7 | ingress-ready: "true" 8 | extraPortMappings: 9 | - containerPort: {{ if (eq .Protocol "http") -}} 80 {{- else -}} 443 {{- end }} 10 | hostPort: {{ .Port }} 11 | protocol: TCP 12 | - containerPort: 10 13 | hostPort: 1000 14 | protocol: TCP 15 | {{ range .ExtraPortsMapping -}} 16 | - containerPort: {{ .ContainerPort }} 17 | hostPort: {{ .HostPort }} 18 | protocol: TCP 19 | {{ end }} 20 | containerdConfigPatches: 21 | - |- 22 | {{ if .UsePathRouting -}} 23 | [plugins."io.containerd.grpc.v1.cri".registry.mirrors."{{ .Host }}:{{ .Port }}"] 24 | endpoint = ["https://{{ .Host }}"] 25 | [plugins."io.containerd.grpc.v1.cri".registry.configs."{{ .Host }}".tls] 26 | insecure_skip_verify = true 27 | {{- else -}} 28 | [plugins."io.containerd.grpc.v1.cri".registry.mirrors."gitea.{{ .Host }}:{{ .Port }}"] 29 | endpoint = ["https://gitea.{{ .Host }}"] 30 | [plugins."io.containerd.grpc.v1.cri".registry.configs."gitea.{{ .Host }}".tls] 31 | insecure_skip_verify = true 32 | {{- end -}} 33 | -------------------------------------------------------------------------------- /hack/gitea/generate-manifests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | INSTALL_YAML="pkg/controllers/localbuild/resources/gitea/k8s/install.yaml" 5 | GITEA_DIR="./hack/gitea" 6 | CHART_VERSION="12.1.2" 7 | 8 | echo "# GITEA INSTALL RESOURCES" >${INSTALL_YAML} 9 | echo "# This file is auto-generated with 'hack/gitea/generate-manifests.sh'" >>${INSTALL_YAML} 10 | 11 | helm repo add gitea-charts --force-update https://dl.gitea.com/charts/ 12 | helm repo update 13 | helm template my-gitea gitea-charts/gitea -f ${GITEA_DIR}/values.yaml --version ${CHART_VERSION} >>${INSTALL_YAML} 14 | sed -i.bak '3d' ${INSTALL_YAML} 15 | 16 | # helm template for pvc uses Release.namespace which doesn't get set 17 | # when running the helm "template" command 18 | # See: https://gitea.com/gitea/helm-chart/issues/630 19 | # and: https://gitea.com/gitea/helm-chart/src/commit/3b2b700441e91a19a535e05de3a9eab2fef0b117/templates/gitea/pvc.yaml#L6 20 | # and: https://github.com/helm/helm/issues/3553#issuecomment-1186518158 21 | # and: https://github.com/splunk/splunk-connect-for-kubernetes/pull/790 22 | sed -i.bak 's/namespace: default/namespace: gitea/g' ${INSTALL_YAML} 23 | 24 | cat ${GITEA_DIR}/ingress.yaml.tmpl >>${INSTALL_YAML} 25 | 26 | rm -rf "${INSTALL_YAML}.bak" 27 | -------------------------------------------------------------------------------- /pkg/printer/types/internal_types.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // Types used internally to define the objects needed to print data, etc 4 | 5 | type Allocated struct { 6 | Cpu string 7 | Memory string 8 | } 9 | 10 | type Capacity struct { 11 | Memory float64 12 | Pods int64 13 | Cpu int64 14 | } 15 | 16 | type Cluster struct { 17 | Name string 18 | URLKubeApi string 19 | KubePort int32 20 | TlsCheck bool 21 | ExternalPort int32 22 | Nodes []Node 23 | } 24 | 25 | type Node struct { 26 | Name string 27 | InternalIP string 28 | ExternalIP string 29 | Capacity Capacity 30 | Allocated Allocated 31 | } 32 | 33 | type Package struct { 34 | Name string 35 | Namespace string 36 | Type string 37 | GitRepository string 38 | ArgocdRepository string 39 | Status string 40 | } 41 | 42 | type Secret struct { 43 | IsCore bool 44 | Name string `json:"name"` 45 | Namespace string `json:"namespace"` 46 | Username string `json:"username,omitempty"` 47 | Password string `json:"password,omitempty"` 48 | Token string `json:"token,omitempty"` 49 | Data map[string]string `json:"data,omitempty"` 50 | } 51 | -------------------------------------------------------------------------------- /hack/ingress-nginx/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.13.0/deploy/static/provider/kind/deploy.yaml 5 | 6 | patches: 7 | - path: deployment-ingress-nginx.yaml 8 | - path: cm-ingress-nginx-controller.yaml 9 | - target: 10 | group: "" 11 | version: v1 12 | kind: Service 13 | name: ingress-nginx-controller 14 | namespace: ingress-nginx 15 | patch: | 16 | $patch: delete 17 | kind: Kustomization 18 | metadata: 19 | name: ingress-nginx-controller 20 | # ArgoCD has poor support for ttlSecondsAfterFinished and it shouldn't be essential to clean these up 21 | - target: 22 | group: batch 23 | version: v1 24 | kind: Job 25 | name: ingress-nginx-admission-create 26 | namespace: ingress-nginx 27 | patch: | 28 | - op: remove 29 | path: /spec/ttlSecondsAfterFinished 30 | - target: 31 | group: batch 32 | version: v1 33 | kind: Job 34 | name: ingress-nginx-admission-patch 35 | namespace: ingress-nginx 36 | patch: | 37 | - op: remove 38 | path: /spec/ttlSecondsAfterFinished 39 | -------------------------------------------------------------------------------- /pkg/controllers/custompackage/test/resources/customPackages/helm/test/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: test 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "1.16.0" 25 | -------------------------------------------------------------------------------- /pkg/cmd/delete/root.go: -------------------------------------------------------------------------------- 1 | package delete 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/cnoe-io/idpbuilder/pkg/cmd/helpers" 7 | "github.com/cnoe-io/idpbuilder/pkg/kind" 8 | "github.com/cnoe-io/idpbuilder/pkg/util" 9 | "github.com/spf13/cobra" 10 | "sigs.k8s.io/kind/pkg/cluster" 11 | ) 12 | 13 | var ( 14 | // Flags 15 | name string 16 | ) 17 | 18 | var DeleteCmd = &cobra.Command{ 19 | Use: "delete", 20 | Short: "Delete an IDP cluster", 21 | Long: ``, 22 | RunE: deleteE, 23 | PreRunE: preDeleteE, 24 | SilenceUsage: true, 25 | } 26 | 27 | func init() { 28 | DeleteCmd.PersistentFlags().StringVar(&name, "name", "localdev", "Name of the kind cluster to be deleted.") 29 | } 30 | 31 | func preDeleteE(cmd *cobra.Command, args []string) error { 32 | return helpers.SetLogger() 33 | } 34 | 35 | func deleteE(cmd *cobra.Command, args []string) error { 36 | logger := helpers.CmdLogger 37 | logger.Info("deleting cluster", "clusterName", name) 38 | detectOpt, err := util.DetectKindNodeProvider() 39 | if err != nil { 40 | return err 41 | } 42 | 43 | provider := cluster.NewProvider(cluster.ProviderWithLogger(kind.KindLoggerFromLogr(&logger)), detectOpt) 44 | if err := provider.Delete(name, ""); err != nil { 45 | return fmt.Errorf("failed to delete cluster %s: %w", name, err) 46 | } 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /pkg/util/k8s.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "github.com/cnoe-io/idpbuilder/pkg/k8s" 6 | "k8s.io/client-go/rest" 7 | "k8s.io/client-go/tools/clientcmd" 8 | "k8s.io/client-go/tools/clientcmd/api" 9 | "k8s.io/client-go/util/homedir" 10 | "path/filepath" 11 | "sigs.k8s.io/controller-runtime/pkg/client" 12 | ) 13 | 14 | var ( 15 | KubeConfigPath string 16 | ) 17 | 18 | func GetKubeConfigPath() string { 19 | if KubeConfigPath == "" { 20 | return filepath.Join(homedir.HomeDir(), ".kube", "config") 21 | } else { 22 | return KubeConfigPath 23 | } 24 | } 25 | 26 | func LoadKubeConfig() (*api.Config, error) { 27 | config, err := clientcmd.LoadFromFile(GetKubeConfigPath()) 28 | if err != nil { 29 | return nil, fmt.Errorf("Failed to load kubeconfig file: %w", err) 30 | } else { 31 | return config, nil 32 | } 33 | } 34 | 35 | func GetKubeConfig() (*rest.Config, error) { 36 | kubeConfig, err := clientcmd.BuildConfigFromFlags("", GetKubeConfigPath()) 37 | if err != nil { 38 | return nil, fmt.Errorf("Error building kubeconfig: %w", err) 39 | } 40 | return kubeConfig, nil 41 | } 42 | 43 | func GetKubeClient(kubeConfig *rest.Config) (client.Client, error) { 44 | kubeClient, err := client.New(kubeConfig, client.Options{Scheme: k8s.GetScheme()}) 45 | if err != nil { 46 | return nil, fmt.Errorf("Error creating kubernetes client: %w", err) 47 | } 48 | return kubeClient, nil 49 | } 50 | -------------------------------------------------------------------------------- /.github/workflows/e2e.yaml: -------------------------------------------------------------------------------- 1 | name: E2E 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | paths: 8 | - '**.go' 9 | - 'go.sum' 10 | - 'go.mod' 11 | repository_dispatch: 12 | types: [e2e-command] 13 | 14 | jobs: 15 | e2e: 16 | runs-on: ubuntu-22.04 17 | if: ${{ github.event.ref != '' }} 18 | steps: 19 | - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 20 | with: 21 | fetch-depth: 0 22 | - name: Setup Go 23 | uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 24 | with: 25 | go-version: '1.22' 26 | - name: Run tests 27 | run: | 28 | make e2e 29 | # invoked by slash command workflow 30 | e2e-slash-command: 31 | runs-on: ubuntu-22.04 32 | if: ${{ github.event.action == 'e2e-command' }} 33 | steps: 34 | - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 35 | with: 36 | repository: ${{ github.event.client_payload.pull_request.head.repo.full_name }} 37 | ref: ${{ github.event.client_payload.pull_request.head.ref }} 38 | fetch-depth: 0 39 | - name: Setup Go 40 | uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 41 | with: 42 | go-version: '1.22' 43 | - name: Run tests 44 | run: | 45 | make e2e 46 | 47 | -------------------------------------------------------------------------------- /pkg/printer/package.go: -------------------------------------------------------------------------------- 1 | package printer 2 | 3 | import ( 4 | "fmt" 5 | "github.com/cnoe-io/idpbuilder/pkg/printer/types" 6 | "io" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | ) 9 | 10 | type PackagePrinter struct { 11 | Packages []types.Package 12 | OutWriter io.Writer 13 | } 14 | 15 | func (pp PackagePrinter) PrintOutput(format string) error { 16 | switch format { 17 | case "json": 18 | return PrintDataAsJson(pp.Packages, pp.OutWriter) 19 | case "yaml": 20 | return PrintDataAsYaml(pp.Packages, pp.OutWriter) 21 | case "table": 22 | return PrintDataAsTable(generatePackageTable(pp.Packages), pp.OutWriter) 23 | default: 24 | return fmt.Errorf("output format %s is not supported", format) 25 | } 26 | } 27 | 28 | func generatePackageTable(packagesTable []types.Package) metav1.Table { 29 | table := &metav1.Table{} 30 | table.ColumnDefinitions = []metav1.TableColumnDefinition{ 31 | {Name: "Name", Type: "string"}, 32 | {Name: "idp namespace", Type: "string"}, 33 | {Name: "Git Repository", Type: "string"}, 34 | {Name: "Argocd Url", Type: "string"}, 35 | {Name: "Status", Type: "string"}, 36 | } 37 | for _, p := range packagesTable { 38 | row := metav1.TableRow{ 39 | Cells: []interface{}{ 40 | p.Name, 41 | p.Namespace, 42 | p.GitRepository, 43 | p.ArgocdRepository, 44 | p.Status, 45 | }, 46 | } 47 | table.Rows = append(table.Rows, row) 48 | } 49 | return *table 50 | } 51 | -------------------------------------------------------------------------------- /hack/gitea/values.yaml: -------------------------------------------------------------------------------- 1 | valkey-cluster: 2 | enabled: false 3 | postgresql: 4 | enabled: false 5 | postgresql-ha: 6 | enabled: false 7 | 8 | persistence: 9 | enabled: true 10 | size: 5Gi 11 | 12 | test: 13 | enabled: false 14 | 15 | gitea: 16 | admin: 17 | existingSecret: gitea-credential 18 | config: 19 | database: 20 | DB_TYPE: sqlite3 21 | session: 22 | PROVIDER: memory 23 | cache: 24 | ADAPTER: memory 25 | queue: 26 | TYPE: level 27 | server: 28 | DOMAIN: '{{- if .UsePathRouting -}} {{ .Host }} {{- else -}} gitea.{{- .Host }} {{- end }}' 29 | ROOT_URL: '{{- if .UsePathRouting }} {{- .Protocol }}://{{ .Host }}:{{ .Port }}/gitea {{- else }} {{- .Protocol }}://gitea.{{ .Host }}:{{ .Port }} {{- end }}' 30 | SSH_PORT: 32222 31 | SSH_LISTEN_PORT: 2222 32 | webhook: 33 | ALLOWED_HOST_LIST: private 34 | SKIP_TLS_VERIFY: true 35 | 36 | service: 37 | http: 38 | type: NodePort 39 | port: 3000 40 | nodePort: 32223 41 | externalTrafficPolicy: Local 42 | ssh: 43 | type: NodePort 44 | port: 32222 45 | nodePort: 32222 46 | externalTrafficPolicy: Local 47 | 48 | ingress: 49 | # NOTE: The ingress is generated in a later step for path based routing feature See: hack/argo-cd/generate-manifests.sh 50 | enabled: false 51 | 52 | image: 53 | pullPolicy: "IfNotPresent" 54 | # Adds -rootless suffix to image name 55 | rootless: true 56 | -------------------------------------------------------------------------------- /pkg/resources/localbuild/application.go: -------------------------------------------------------------------------------- 1 | package localbuild 2 | 3 | import ( 4 | argov1alpha1 "github.com/cnoe-io/argocd-api/api/argo/application/v1alpha1" 5 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 6 | ) 7 | 8 | func SetProjectSpec(project *argov1alpha1.AppProject) { 9 | project.Spec.Description = "IDP Builder Project" 10 | 11 | project.Spec.ClusterResourceWhitelist = []v1.GroupKind{{ 12 | Group: "*", 13 | Kind: "*", 14 | }} 15 | project.Spec.NamespaceResourceWhitelist = []v1.GroupKind{{ 16 | Group: "*", 17 | Kind: "*", 18 | }} 19 | 20 | project.Spec.Destinations = []argov1alpha1.ApplicationDestination{{ 21 | Name: "*", 22 | Namespace: "*", 23 | Server: "*", 24 | }} 25 | 26 | project.Spec.SourceRepos = []string{ 27 | "*", 28 | } 29 | } 30 | 31 | func SetApplicationSpec(app *argov1alpha1.Application, repoUrl, path, project, dstNS string, targetRevision *string) { 32 | headRev := "HEAD" 33 | if targetRevision == nil { 34 | targetRevision = &headRev 35 | } 36 | 37 | app.Spec.Destination = argov1alpha1.ApplicationDestination{ 38 | Server: "https://kubernetes.default.svc", 39 | Namespace: dstNS, 40 | } 41 | 42 | app.Spec.Project = project 43 | 44 | app.Spec.Source = &argov1alpha1.ApplicationSource{ 45 | Path: path, 46 | RepoURL: repoUrl, 47 | TargetRevision: *targetRevision, 48 | } 49 | 50 | app.Spec.SyncPolicy = &argov1alpha1.SyncPolicy{ 51 | Automated: &argov1alpha1.SyncPolicyAutomated{ 52 | SelfHeal: true, 53 | }, 54 | SyncOptions: argov1alpha1.SyncOptions{ 55 | "CreateNamespace=true", 56 | }, 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /pkg/controllers/localbuild/nginx.go: -------------------------------------------------------------------------------- 1 | package localbuild 2 | 3 | import ( 4 | "context" 5 | "embed" 6 | 7 | "github.com/cnoe-io/idpbuilder/api/v1alpha1" 8 | "github.com/cnoe-io/idpbuilder/globals" 9 | "github.com/cnoe-io/idpbuilder/pkg/k8s" 10 | "k8s.io/apimachinery/pkg/runtime" 11 | "k8s.io/apimachinery/pkg/runtime/schema" 12 | ctrl "sigs.k8s.io/controller-runtime" 13 | ) 14 | 15 | //go:embed resources/nginx/k8s/* 16 | var installNginxFS embed.FS 17 | 18 | func RawNginxInstallResources(templateData any, config v1alpha1.PackageCustomization, scheme *runtime.Scheme) ([][]byte, error) { 19 | return k8s.BuildCustomizedManifests(config.FilePath, "resources/nginx/k8s", installNginxFS, scheme, templateData) 20 | } 21 | 22 | func (r *LocalbuildReconciler) ReconcileNginx(ctx context.Context, req ctrl.Request, resource *v1alpha1.Localbuild) (ctrl.Result, error) { 23 | nginx := EmbeddedInstallation{ 24 | name: "Nginx", 25 | resourcePath: "resources/nginx/k8s", 26 | resourceFS: installNginxFS, 27 | namespace: globals.NginxNamespace, 28 | monitoredResources: map[string]schema.GroupVersionKind{ 29 | "ingress-nginx-controller": { 30 | Group: "apps", 31 | Version: "v1", 32 | Kind: "Deployment", 33 | }, 34 | }, 35 | } 36 | 37 | v, ok := resource.Spec.PackageConfigs.CorePackageCustomization[v1alpha1.IngressNginxPackageName] 38 | if ok { 39 | nginx.customization = v 40 | } 41 | 42 | if result, err := nginx.Install(ctx, resource, r.Client, r.Scheme, r.Config); err != nil { 43 | return result, err 44 | } 45 | 46 | resource.Status.Nginx.Available = true 47 | return ctrl.Result{}, nil 48 | } 49 | -------------------------------------------------------------------------------- /hack/ingress-nginx/deployment-ingress-nginx.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: ingress-nginx-controller 5 | namespace: ingress-nginx 6 | spec: 7 | strategy: 8 | rollingUpdate: 9 | maxUnavailable: 1 10 | type: RollingUpdate 11 | template: 12 | metadata: 13 | labels: 14 | app.kubernetes.io/component: controller 15 | app.kubernetes.io/instance: ingress-nginx 16 | app.kubernetes.io/name: ingress-nginx 17 | app.kubernetes.io/part-of: ingress-nginx 18 | app.kubernetes.io/version: 1.8.1 19 | spec: 20 | terminationGracePeriodSeconds: 0 21 | containers: 22 | - name: controller 23 | args: 24 | - /nginx-ingress-controller 25 | - --election-id=ingress-nginx-leader 26 | - --controller-class=k8s.io/ingress-nginx 27 | - --ingress-class=nginx 28 | - --configmap=$(POD_NAMESPACE)/ingress-nginx-controller 29 | - --validating-webhook=:8443 30 | - --validating-webhook-certificate=/usr/local/certificates/cert 31 | - --validating-webhook-key=/usr/local/certificates/key 32 | - --watch-ingress-without-class=true 33 | - --publish-status-address=localhost 34 | - --enable-ssl-passthrough 35 | - --default-ssl-certificate=ingress-nginx/idpbuilder-cert 36 | ports: 37 | - containerPort: 80 38 | hostPort: 80 39 | name: http 40 | protocol: TCP 41 | - containerPort: 443 42 | hostPort: 443 43 | name: https 44 | protocol: TCP 45 | -------------------------------------------------------------------------------- /pkg/printer/secret.go: -------------------------------------------------------------------------------- 1 | package printer 2 | 3 | import ( 4 | "fmt" 5 | "github.com/cnoe-io/idpbuilder/pkg/printer/types" 6 | "io" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "strings" 9 | ) 10 | 11 | type SecretPrinter struct { 12 | Secrets []types.Secret 13 | OutWriter io.Writer 14 | } 15 | 16 | func (sp SecretPrinter) PrintOutput(format string) error { 17 | switch format { 18 | case "json": 19 | return PrintDataAsJson(sp.Secrets, sp.OutWriter) 20 | case "yaml": 21 | return PrintDataAsYaml(sp.Secrets, sp.OutWriter) 22 | case "table": 23 | return PrintDataAsTable(generateSecretTable(sp.Secrets), sp.OutWriter) 24 | default: 25 | return fmt.Errorf("output format %s is not supported", format) 26 | } 27 | } 28 | 29 | func generateSecretTable(secretTable []types.Secret) metav1.Table { 30 | table := &metav1.Table{} 31 | table.ColumnDefinitions = []metav1.TableColumnDefinition{ 32 | {Name: "Name", Type: "string"}, 33 | {Name: "Namespace", Type: "string"}, 34 | {Name: "Username", Type: "string"}, 35 | {Name: "Password", Type: "string"}, 36 | {Name: "Token", Type: "string"}, 37 | {Name: "Data", Type: "string"}, 38 | } 39 | for _, secret := range secretTable { 40 | var dataEntries []string 41 | 42 | if !secret.IsCore { 43 | for key, value := range secret.Data { 44 | dataEntries = append(dataEntries, fmt.Sprintf("%s=%s", key, value)) 45 | } 46 | } 47 | dataString := strings.Join(dataEntries, ", ") 48 | row := metav1.TableRow{ 49 | Cells: []interface{}{ 50 | secret.Name, 51 | secret.Namespace, 52 | secret.Username, 53 | secret.Password, 54 | secret.Token, 55 | dataString, 56 | }, 57 | } 58 | table.Rows = append(table.Rows, row) 59 | } 60 | return *table 61 | } 62 | -------------------------------------------------------------------------------- /pkg/cmd/helpers/logger.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "os" 7 | "strings" 8 | 9 | "github.com/cnoe-io/idpbuilder/pkg/logger" 10 | "github.com/go-logr/logr" 11 | "k8s.io/klog/v2" 12 | ctrl "sigs.k8s.io/controller-runtime" 13 | ) 14 | 15 | var ( 16 | LogLevel string 17 | LogLevelMsg = "Set the log verbosity. Supported values are: debug, info, warn, and error." 18 | CmdLogger logr.Logger 19 | ColoredOutput bool 20 | ColoredOutputMsg = "Enable colored log messages." 21 | ) 22 | 23 | func SetLogger() error { 24 | l, err := getSlogLevel(LogLevel) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | slogger := slog.New(logger.NewHandler(os.Stderr, logger.Options{Level: l, Colored: ColoredOutput})) 30 | kslogger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: getKlogLevel(l)})) 31 | logger := logr.FromSlogHandler(slogger.Handler()) 32 | klogger := logr.FromSlogHandler(kslogger.Handler()) 33 | 34 | klog.SetLogger(klogger) 35 | ctrl.SetLogger(logger) 36 | CmdLogger = logger 37 | return nil 38 | } 39 | 40 | func getSlogLevel(s string) (slog.Level, error) { 41 | switch strings.ToLower(s) { 42 | case "debug": 43 | return slog.LevelDebug, nil 44 | case "info": 45 | return slog.LevelInfo, nil 46 | case "warn": 47 | return slog.LevelWarn, nil 48 | case "error": 49 | return slog.LevelError, nil 50 | default: 51 | return slog.LevelDebug, fmt.Errorf("%s is not a valid log level", s) 52 | } 53 | } 54 | 55 | // For end users, klog messages are mostly useless. We set it to error level unless debug logging is enabled. 56 | func getKlogLevel(l slog.Level) slog.Level { 57 | if l < slog.LevelInfo { 58 | return l 59 | } 60 | return slog.LevelError 61 | } 62 | -------------------------------------------------------------------------------- /pkg/printer/cluster.go: -------------------------------------------------------------------------------- 1 | package printer 2 | 3 | import ( 4 | "fmt" 5 | "github.com/cnoe-io/idpbuilder/pkg/printer/types" 6 | "io" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | ) 9 | 10 | type ClusterPrinter struct { 11 | Clusters []types.Cluster 12 | OutWriter io.Writer 13 | } 14 | 15 | func (cp ClusterPrinter) PrintOutput(format string) error { 16 | switch format { 17 | case "json": 18 | return PrintDataAsJson(cp.Clusters, cp.OutWriter) 19 | case "yaml": 20 | return PrintDataAsYaml(cp.Clusters, cp.OutWriter) 21 | case "table": 22 | return PrintDataAsTable(generateClusterTable(cp.Clusters), cp.OutWriter) 23 | default: 24 | return fmt.Errorf("output format %s is not supported", format) 25 | } 26 | } 27 | 28 | func generateClusterTable(input []types.Cluster) metav1.Table { 29 | table := &metav1.Table{} 30 | table.ColumnDefinitions = []metav1.TableColumnDefinition{ 31 | {Name: "Name", Type: "string"}, 32 | {Name: "External-Port", Type: "string"}, 33 | {Name: "Kube-Api", Type: "string"}, 34 | {Name: "TLS", Type: "string"}, 35 | {Name: "Kube-Port", Type: "string"}, 36 | {Name: "Nodes", Type: "string"}, 37 | } 38 | 39 | for _, cluster := range input { 40 | row := metav1.TableRow{ 41 | Cells: []interface{}{ 42 | cluster.Name, 43 | cluster.ExternalPort, 44 | cluster.URLKubeApi, 45 | cluster.TlsCheck, 46 | cluster.KubePort, 47 | generateNodeData(cluster.Nodes), 48 | }, 49 | } 50 | table.Rows = append(table.Rows, row) 51 | } 52 | return *table 53 | } 54 | 55 | func generateNodeData(nodes []types.Node) string { 56 | var result string 57 | for i, aNode := range nodes { 58 | result += aNode.Name 59 | if i < len(nodes)-1 { 60 | result += "," 61 | } 62 | } 63 | return result 64 | } 65 | -------------------------------------------------------------------------------- /pkg/k8s/client.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | corev1 "k8s.io/api/core/v1" 7 | k8serrors "k8s.io/apimachinery/pkg/api/errors" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 10 | "k8s.io/apimachinery/pkg/types" 11 | "sigs.k8s.io/controller-runtime/pkg/client" 12 | ) 13 | 14 | func EnsureObject(ctx context.Context, kubeClient client.Client, obj client.Object, namespace string) error { 15 | curObj := &unstructured.Unstructured{} 16 | curObj.SetGroupVersionKind(obj.GetObjectKind().GroupVersionKind()) 17 | 18 | // Fallback to object's namespace 19 | if namespace == "" { 20 | namespace = obj.GetNamespace() 21 | } 22 | 23 | // Get Object if it exists 24 | err := kubeClient.Get( 25 | ctx, 26 | types.NamespacedName{ 27 | Namespace: namespace, 28 | Name: obj.GetName(), 29 | }, 30 | curObj, 31 | ) 32 | 33 | if err == nil { 34 | // Object already exists 35 | return nil 36 | } 37 | 38 | err = kubeClient.Create(ctx, obj) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | // hacky way to restore the GVK for the object after create corrupts it. didn't dig. not sure why? 44 | obj.GetObjectKind().SetGroupVersionKind(curObj.GroupVersionKind()) 45 | return nil 46 | } 47 | 48 | func EnsureNamespace(ctx context.Context, kubeClient client.Client, name string) error { 49 | ns := &corev1.Namespace{ 50 | ObjectMeta: metav1.ObjectMeta{ 51 | Name: name, 52 | }, 53 | } 54 | 55 | err := kubeClient.Get(ctx, client.ObjectKeyFromObject(ns), ns) 56 | if err != nil { 57 | if k8serrors.IsNotFound(err) { 58 | return kubeClient.Create(ctx, ns) 59 | } else { 60 | return fmt.Errorf("getting namespace %s: %w", name, err) 61 | } 62 | } 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /pkg/k8s/util.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "embed" 5 | "github.com/cnoe-io/idpbuilder/pkg/util/files" 6 | "github.com/cnoe-io/idpbuilder/pkg/util/fs" 7 | "k8s.io/apimachinery/pkg/runtime" 8 | "os" 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | ) 11 | 12 | func BuildCustomizedManifests(filePath, fsPath string, resourceFS embed.FS, scheme *runtime.Scheme, templateData any) ([][]byte, error) { 13 | rawResources, err := fs.ConvertFSToBytes(resourceFS, fsPath, templateData) 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | if filePath == "" { 19 | return rawResources, nil 20 | } 21 | 22 | bs, _, err := applyOverrides(filePath, rawResources, scheme, templateData) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | return bs, nil 28 | } 29 | 30 | func BuildCustomizedObjects(filePath, fsPath string, resourceFS embed.FS, scheme *runtime.Scheme, templateData any) ([]client.Object, error) { 31 | rawResources, err := fs.ConvertFSToBytes(resourceFS, fsPath, templateData) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | if filePath == "" { 37 | return ConvertRawResourcesToObjects(scheme, rawResources) 38 | } 39 | 40 | _, objs, err := applyOverrides(filePath, rawResources, scheme, templateData) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | return objs, nil 46 | } 47 | 48 | func applyOverrides(filePath string, originalFiles [][]byte, scheme *runtime.Scheme, templateData any) ([][]byte, []client.Object, error) { 49 | customBS, err := os.ReadFile(filePath) 50 | if err != nil { 51 | return nil, nil, err 52 | } 53 | 54 | rendered, err := files.ApplyTemplate(customBS, templateData) 55 | if err != nil { 56 | return nil, nil, err 57 | } 58 | 59 | return ConvertYamlToObjectsWithOverride(scheme, originalFiles, rendered) 60 | } 61 | -------------------------------------------------------------------------------- /pkg/controllers/localbuild/argo.go: -------------------------------------------------------------------------------- 1 | package localbuild 2 | 3 | import ( 4 | "context" 5 | "embed" 6 | "github.com/cnoe-io/idpbuilder/api/v1alpha1" 7 | "github.com/cnoe-io/idpbuilder/globals" 8 | "github.com/cnoe-io/idpbuilder/pkg/k8s" 9 | "k8s.io/apimachinery/pkg/runtime" 10 | "k8s.io/apimachinery/pkg/runtime/schema" 11 | ctrl "sigs.k8s.io/controller-runtime" 12 | ) 13 | 14 | //go:embed resources/argo/* 15 | var installArgoFS embed.FS 16 | 17 | func RawArgocdInstallResources(templateData any, config v1alpha1.PackageCustomization, scheme *runtime.Scheme) ([][]byte, error) { 18 | return k8s.BuildCustomizedManifests(config.FilePath, "resources/argo", installArgoFS, scheme, templateData) 19 | } 20 | 21 | func (r *LocalbuildReconciler) ReconcileArgo(ctx context.Context, req ctrl.Request, resource *v1alpha1.Localbuild) (ctrl.Result, error) { 22 | argocd := EmbeddedInstallation{ 23 | name: "Argo CD", 24 | resourcePath: "resources/argo", 25 | resourceFS: installArgoFS, 26 | namespace: globals.ArgoCDNamespace, 27 | monitoredResources: map[string]schema.GroupVersionKind{ 28 | "argocd-server": { 29 | Group: "apps", 30 | Version: "v1", 31 | Kind: "Deployment", 32 | }, 33 | "argocd-repo-server": { 34 | Group: "apps", 35 | Version: "v1", 36 | Kind: "Deployment", 37 | }, 38 | "argocd-application-controller": { 39 | Group: "apps", 40 | Version: "v1", 41 | Kind: "StatefulSet", 42 | }, 43 | }, 44 | skipReadinessCheck: true, 45 | } 46 | 47 | v, ok := resource.Spec.PackageConfigs.CorePackageCustomization[v1alpha1.ArgoCDPackageName] 48 | if ok { 49 | argocd.customization = v 50 | } 51 | 52 | if result, err := argocd.Install(ctx, resource, r.Client, r.Scheme, r.Config); err != nil { 53 | return result, err 54 | } 55 | 56 | resource.Status.ArgoCD.Available = true 57 | return ctrl.Result{}, nil 58 | } 59 | -------------------------------------------------------------------------------- /pkg/util/fs/fs_test.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "fmt" 5 | "io/fs" 6 | "os" 7 | "path/filepath" 8 | "testing" 9 | "testing/fstest" 10 | 11 | "github.com/cnoe-io/idpbuilder/globals" 12 | "github.com/google/go-cmp/cmp" 13 | ) 14 | 15 | func TestWriteFS(t *testing.T) { 16 | cases := []struct { 17 | name string 18 | srcFS fs.FS 19 | expectErr error 20 | expectFiles map[string][]byte 21 | }{{ 22 | name: "single file", 23 | srcFS: fstest.MapFS{ 24 | "testfile": &fstest.MapFile{ 25 | Data: []byte("testdata"), 26 | Mode: 0666, 27 | }, 28 | }, 29 | expectFiles: map[string][]byte{ 30 | "testfile": []byte("testdata"), 31 | }, 32 | }, { 33 | name: "file in subdir", 34 | srcFS: fstest.MapFS{ 35 | "somedir": &fstest.MapFile{ 36 | Mode: fs.ModeDir, 37 | }, 38 | "somedir/testfile": &fstest.MapFile{ 39 | Data: []byte("testdata"), 40 | Mode: 0666, 41 | }, 42 | }, 43 | expectFiles: map[string][]byte{ 44 | "somedir/testfile": []byte("testdata"), 45 | }, 46 | }} 47 | 48 | for _, tc := range cases { 49 | t.Run(tc.name, func(t *testing.T) { 50 | workDir, err := os.MkdirTemp("", fmt.Sprintf("%s-fs_test.go-%s", globals.ProjectName, tc.name)) 51 | if err != nil { 52 | t.Fatalf("creating tempdir: %v", err) 53 | } 54 | defer os.RemoveAll(workDir) 55 | 56 | err = WriteFS(tc.srcFS, workDir) 57 | if err != tc.expectErr { 58 | t.Errorf("unexpected error writing fs: %v", err) 59 | } 60 | 61 | for expectPath, expectData := range tc.expectFiles { 62 | fullExpectPath := filepath.Join(workDir, expectPath) 63 | gotData, err := os.ReadFile(fullExpectPath) 64 | if err != nil { 65 | t.Errorf("Opening expected file: %v", err) 66 | } 67 | 68 | if diff := cmp.Diff(string(expectData), string(gotData)); diff != "" { 69 | t.Errorf("Expected data in %q mismatch (-want +got):\n%s", expectPath, diff) 70 | } 71 | } 72 | }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: 5 | - 'v[0-9]+.[0-9]+.[0-9]+' 6 | - 'v[0-9]+.[0-9]+.[0-9]+-nightly.[0-9]+' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-22.04 14 | steps: 15 | - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 16 | with: 17 | fetch-depth: 0 18 | - run: git fetch --force --tags 19 | - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 20 | with: 21 | go-version: '1.22' 22 | - name: Set GORELEASER_PREVIOUS_TAG in actual release 23 | if: ${{ !contains(github.ref, '-nightly') }} 24 | # find previous tag by filtering out nightly tags and choosing the 25 | # second to last tag (last one is the current release) 26 | run: | 27 | prev_tag=$(git tag | grep -v "nightly" | sort -r --version-sort | head -n 2 | tail -n 1) 28 | echo "GORELEASER_PREVIOUS_TAG=$prev_tag" >> $GITHUB_ENV 29 | # Ensure generation tools run 30 | - name: build 31 | run: | 32 | OUT_FILE=/tmp/idpbuilder make build 33 | - name: Generate a homebrew tap update token 34 | id: generate-token 35 | uses: actions/create-github-app-token@v1 36 | with: 37 | app-id: ${{ vars.CNOE_HOMEBREW_APP_ID }} 38 | private-key: ${{ secrets.CNOE_HOMEBREW_PRIVATE_KEY }} 39 | repositories: | 40 | homebrew-tap 41 | - name: GoReleaser 42 | uses: goreleaser/goreleaser-action@7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8 # v5.0.0 43 | id: run-goreleaser 44 | with: 45 | version: latest 46 | args: release --clean --timeout 30m 47 | env: 48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | HOMEBREW_TOKEN: ${{ steps.generate-token.outputs.token }} 50 | GORELEASER_CURRENT_TAG: ${{ github.ref_name }} 51 | -------------------------------------------------------------------------------- /pkg/build/build_test.go: -------------------------------------------------------------------------------- 1 | package build 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/cnoe-io/idpbuilder/api/v1alpha1" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/mock" 10 | "github.com/stretchr/testify/require" 11 | k8serrors "k8s.io/apimachinery/pkg/api/errors" 12 | "k8s.io/apimachinery/pkg/runtime/schema" 13 | "sigs.k8s.io/controller-runtime/pkg/client" 14 | ) 15 | 16 | func TestIsCompatible(t *testing.T) { 17 | cfg := v1alpha1.BuildCustomizationSpec{ 18 | Protocol: "http", 19 | Host: "cnoe.localtest.me", 20 | IngressHost: "string", 21 | Port: "8443", 22 | UsePathRouting: false, 23 | SelfSignedCert: "some-cert", 24 | } 25 | 26 | b := Build{ 27 | name: "test", 28 | cfg: cfg, 29 | } 30 | 31 | ctx := context.Background() 32 | fClient := new(fakeKubeClient) 33 | fClient.On("Get", ctx, client.ObjectKey{Name: "test"}, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { 34 | arg := args.Get(2).(*v1alpha1.Localbuild) 35 | arg.Spec.BuildCustomization = cfg 36 | }).Return(nil) 37 | 38 | ok, err := b.isCompatible(ctx, fClient) 39 | 40 | assert.NoError(t, err) 41 | fClient.AssertExpectations(t) 42 | require.True(t, ok) 43 | 44 | fClient = new(fakeKubeClient) 45 | fClient.On("Get", ctx, client.ObjectKey{Name: "test"}, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { 46 | arg := args.Get(2).(*v1alpha1.Localbuild) 47 | c := cfg 48 | c.Host = "not-right" 49 | arg.Spec.BuildCustomization = c 50 | }).Return(nil) 51 | 52 | ok, err = b.isCompatible(ctx, fClient) 53 | 54 | assert.Error(t, err) 55 | fClient.AssertExpectations(t) 56 | require.False(t, ok) 57 | 58 | fClient = new(fakeKubeClient) 59 | fClient.On("Get", ctx, client.ObjectKey{Name: "test"}, mock.Anything, mock.Anything). 60 | Return(k8serrors.NewNotFound(schema.GroupResource{}, "name")) 61 | 62 | ok, err = b.isCompatible(ctx, fClient) 63 | 64 | assert.NoError(t, err) 65 | fClient.AssertExpectations(t) 66 | require.True(t, ok) 67 | } 68 | -------------------------------------------------------------------------------- /hack/argo-cd/ingress.yaml.tmpl: -------------------------------------------------------------------------------- 1 | {{- if .UsePathRouting -}} 2 | --- 3 | apiVersion: networking.k8s.io/v1 4 | kind: Ingress 5 | metadata: 6 | name: argocd-server-ingress-http 7 | namespace: argocd 8 | annotations: 9 | nginx.ingress.kubernetes.io/backend-protocol: "HTTP" 10 | nginx.ingress.kubernetes.io/use-regex: "true" 11 | nginx.ingress.kubernetes.io/rewrite-target: /$2 12 | spec: 13 | ingressClassName: nginx 14 | rules: 15 | - host: {{ .IngressHost }} 16 | http: 17 | paths: 18 | - path: /argocd(/|$)(.*) 19 | pathType: ImplementationSpecific 20 | backend: 21 | service: 22 | name: argocd-server 23 | port: 24 | name: http 25 | {{- if ne .IngressHost .Host }} 26 | - host: {{ .Host }} 27 | http: 28 | paths: 29 | - path: /argocd(/|$)(.*) 30 | pathType: ImplementationSpecific 31 | backend: 32 | service: 33 | name: argocd-server 34 | port: 35 | name: http 36 | {{ end }} 37 | {{- else -}} 38 | --- 39 | apiVersion: networking.k8s.io/v1 40 | kind: Ingress 41 | metadata: 42 | name: argocd-server-ingress 43 | namespace: argocd 44 | annotations: 45 | nginx.ingress.kubernetes.io/force-ssl-redirect: "true" 46 | nginx.ingress.kubernetes.io/ssl-passthrough: "true" 47 | spec: 48 | ingressClassName: "nginx" 49 | rules: 50 | - host: argocd.{{ .IngressHost }} 51 | http: 52 | paths: 53 | - path: / 54 | pathType: Prefix 55 | backend: 56 | service: 57 | name: argocd-server 58 | port: 59 | name: https 60 | {{- if ne .IngressHost .Host }} 61 | - host: argocd.{{ .Host }} 62 | http: 63 | paths: 64 | - path: / 65 | pathType: Prefix 66 | backend: 67 | service: 68 | name: argocd-server 69 | port: 70 | name: https 71 | {{ end }} 72 | {{ end }} 73 | -------------------------------------------------------------------------------- /pkg/controllers/localbuild/resources/argo/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .UsePathRouting -}} 2 | --- 3 | apiVersion: networking.k8s.io/v1 4 | kind: Ingress 5 | metadata: 6 | name: argocd-server-ingress-http 7 | namespace: argocd 8 | annotations: 9 | nginx.ingress.kubernetes.io/backend-protocol: "HTTP" 10 | nginx.ingress.kubernetes.io/use-regex: "true" 11 | nginx.ingress.kubernetes.io/rewrite-target: /$2 12 | spec: 13 | ingressClassName: nginx 14 | rules: 15 | - host: {{ .IngressHost }} 16 | http: 17 | paths: 18 | - path: /argocd(/|$)(.*) 19 | pathType: ImplementationSpecific 20 | backend: 21 | service: 22 | name: argocd-server 23 | port: 24 | name: http 25 | {{- if ne .IngressHost .Host }} 26 | - host: {{ .Host }} 27 | http: 28 | paths: 29 | - path: /argocd(/|$)(.*) 30 | pathType: ImplementationSpecific 31 | backend: 32 | service: 33 | name: argocd-server 34 | port: 35 | name: http 36 | {{ end }} 37 | {{- else -}} 38 | --- 39 | apiVersion: networking.k8s.io/v1 40 | kind: Ingress 41 | metadata: 42 | name: argocd-server-ingress 43 | namespace: argocd 44 | annotations: 45 | nginx.ingress.kubernetes.io/force-ssl-redirect: "true" 46 | nginx.ingress.kubernetes.io/ssl-passthrough: "true" 47 | spec: 48 | ingressClassName: "nginx" 49 | rules: 50 | - host: argocd.{{ .IngressHost }} 51 | http: 52 | paths: 53 | - path: / 54 | pathType: Prefix 55 | backend: 56 | service: 57 | name: argocd-server 58 | port: 59 | name: https 60 | {{- if ne .IngressHost .Host }} 61 | - host: argocd.{{ .Host }} 62 | http: 63 | paths: 64 | - path: / 65 | pathType: Prefix 66 | backend: 67 | service: 68 | name: argocd-server 69 | port: 70 | name: https 71 | {{ end }} 72 | {{ end }} 73 | -------------------------------------------------------------------------------- /pkg/kind/kindlogger.go: -------------------------------------------------------------------------------- 1 | package kind 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/go-logr/logr" 7 | kindlog "sigs.k8s.io/kind/pkg/log" 8 | ) 9 | 10 | // this is a wrapper of logr.Logger made specifically for kind' logger. 11 | // this is needed because kind's implementation is internal. 12 | // https://github.com/kubernetes-sigs/kind/blob/1a8f0473a0785e0975e26739524513e8ee696be3/pkg/log/types.go 13 | type kindLogger struct { 14 | cliLogger *logr.Logger 15 | } 16 | 17 | func (l *kindLogger) Warn(message string) { 18 | l.cliLogger.Info(message) 19 | } 20 | 21 | func (l *kindLogger) Warnf(message string, args ...interface{}) { 22 | l.cliLogger.Info(fmt.Sprintf(message, args...)) 23 | } 24 | 25 | func (l *kindLogger) Error(message string) { 26 | l.cliLogger.Error(fmt.Errorf(message), "") 27 | } 28 | 29 | func (l *kindLogger) Errorf(message string, args ...interface{}) { 30 | msg := fmt.Sprintf(message, args...) 31 | l.cliLogger.Error(fmt.Errorf(msg), "") 32 | } 33 | 34 | func (l *kindLogger) V(level kindlog.Level) kindlog.InfoLogger { 35 | return newKindInfoLogger(l.cliLogger, int(level)) 36 | } 37 | 38 | // KindLoggerFromLogr is a wrapper of logr.Logger made specifically for kind's InfoLogger. 39 | // https://github.com/kubernetes-sigs/kind/blob/1a8f0473a0785e0975e26739524513e8ee696be3/pkg/log/types.go 40 | func KindLoggerFromLogr(logrLogger *logr.Logger) *kindLogger { 41 | return &kindLogger{ 42 | cliLogger: logrLogger, 43 | } 44 | } 45 | 46 | func newKindInfoLogger(logrLogger *logr.Logger, level int) *kindInfoLogger { 47 | return &kindInfoLogger{ 48 | cliLogger: logrLogger, 49 | level: level + 1, // push log level down. e.g. info log becomes debug+1. 50 | } 51 | } 52 | 53 | type kindInfoLogger struct { 54 | cliLogger *logr.Logger 55 | level int 56 | } 57 | 58 | func (k *kindInfoLogger) Info(message string) { 59 | k.cliLogger.V(k.level).Info(message) 60 | } 61 | 62 | func (k *kindInfoLogger) Infof(message string, args ...interface{}) { 63 | k.cliLogger.V(k.level).Info(fmt.Sprintf(message, args...)) 64 | } 65 | 66 | func (k *kindInfoLogger) Enabled() bool { 67 | return k.cliLogger.Enabled() 68 | } 69 | -------------------------------------------------------------------------------- /.devcontainer/postCreateCommand.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # For Kubectl AMD64 / x86_64 4 | [ $(uname -m) = x86_64 ] && curl -sLO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" 5 | # For Kubectl ARM64 6 | [ $(uname -m) = aarch64 ] && curl -sLO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/arm64/kubectl" 7 | chmod +x ./kubectl 8 | sudo mv ./kubectl /usr/local/bin/kubectl 9 | 10 | # For Kind AMD64 / x86_64 11 | [ $(uname -m) = x86_64 ] && curl -sLo ./kind https://kind.sigs.k8s.io/dl/v0.22.0/kind-linux-amd64 12 | # For Kind ARM64 13 | [ $(uname -m) = aarch64 ] && curl -sLo ./kind https://kind.sigs.k8s.io/dl/v0.22.0/kind-linux-arm64 14 | chmod +x ./kind 15 | sudo mv ./kind /usr/local/bin/kind 16 | 17 | # setup autocomplete for kubectl and alias k 18 | sudo apt-get update -y && sudo apt-get install bash-completion -y 19 | mkdir $HOME/.kube 20 | echo "source <(kubectl completion bash)" >> $HOME/.bashrc 21 | echo "alias k=kubectl" >> $HOME/.bashrc 22 | echo "complete -F __start_kubectl k" >> $HOME/.bashrc 23 | 24 | # Configure git if environment variables are set 25 | if [ -n "$GIT_COMMITER_NAME" ]; then 26 | echo "Configuring git user.name to: $GIT_COMMITER_NAME" 27 | git config --global user.name "$GIT_COMMITER_NAME" 28 | fi 29 | 30 | if [ -n "$GIT_COMMITER_EMAIL" ]; then 31 | echo "Configuring git user.email to: $GIT_COMMITER_EMAIL" 32 | git config --global user.email "$GIT_COMMITER_EMAIL" 33 | fi 34 | 35 | # 1. Configure GPG agent 36 | mkdir -p ~/.gnupg 37 | echo "pinentry-program /usr/bin/pinentry" > ~/.gnupg/gpg-agent.conf 38 | echo "allow-loopback-pinentry" >> ~/.gnupg/gpg-agent.conf 39 | 40 | # 2. Configure GPG client 41 | echo "use-agent" > ~/.gnupg/gpg.conf 42 | echo "pinentry-mode loopback" >> ~/.gnupg/gpg.conf 43 | 44 | # 3. Restart GPG agent and set environment 45 | gpgconf --kill gpg-agent 46 | export GPG_TTY=$(tty) 47 | echo 'export GPG_TTY=$(tty)' >> ~/.bashrc 48 | 49 | # 4. Configure Git for GPG signing 50 | git config --global commit.gpgsign true 51 | git config --global tag.gpgsign true 52 | git config --global gpg.program gpg 53 | -------------------------------------------------------------------------------- /pkg/k8s/deserialize_test.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/go-cmp/cmp" 7 | appsv1 "k8s.io/api/apps/v1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | "k8s.io/apimachinery/pkg/runtime" 10 | "sigs.k8s.io/controller-runtime/pkg/client" 11 | ) 12 | 13 | func newDeployment(name string) *appsv1.Deployment { 14 | return &appsv1.Deployment{ 15 | TypeMeta: metav1.TypeMeta{ 16 | Kind: "Deployment", 17 | APIVersion: appsv1.SchemeGroupVersion.Identifier(), 18 | }, 19 | ObjectMeta: metav1.ObjectMeta{ 20 | Name: name, 21 | }, 22 | } 23 | } 24 | 25 | func TestConvertYamlToObjects(t *testing.T) { 26 | cases := []struct { 27 | name string 28 | schemeBuilder runtime.SchemeBuilder 29 | input string 30 | expectErr error 31 | expectObjects []client.Object 32 | }{{ 33 | name: "Single Deployment", 34 | schemeBuilder: appsv1.SchemeBuilder, 35 | input: ` 36 | apiVersion: apps/v1 37 | kind: Deployment 38 | metadata: 39 | name: test-deployment1 40 | spec:`, 41 | expectErr: nil, 42 | expectObjects: []client.Object{ 43 | newDeployment("test-deployment1"), 44 | }, 45 | }, { 46 | name: "Multi Deployment", 47 | schemeBuilder: appsv1.SchemeBuilder, 48 | input: ` 49 | apiVersion: apps/v1 50 | kind: Deployment 51 | metadata: 52 | name: test-deployment1 53 | spec: 54 | --- 55 | apiVersion: apps/v1 56 | kind: Deployment 57 | metadata: 58 | name: test-deployment2 59 | spec:`, 60 | expectErr: nil, 61 | expectObjects: []client.Object{ 62 | newDeployment("test-deployment1"), 63 | newDeployment("test-deployment2"), 64 | }, 65 | }} 66 | 67 | for _, tc := range cases { 68 | t.Run(tc.name, func(t *testing.T) { 69 | scheme := runtime.NewScheme() 70 | tc.schemeBuilder.AddToScheme(scheme) 71 | objs, err := ConvertYamlToObjects(scheme, []byte(tc.input)) 72 | 73 | if err != tc.expectErr { 74 | t.Fatalf("want err: %v, got err %v", tc.expectErr, err) 75 | } 76 | 77 | if diff := cmp.Diff(tc.expectObjects, objs); diff != "" { 78 | t.Errorf("ConvertYamlToObjects() mismatch (-want +got):\n%s", diff) 79 | } 80 | }) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /pkg/controllers/gitrepository/git_repository.go: -------------------------------------------------------------------------------- 1 | package gitrepository 2 | 3 | import ( 4 | "context" 5 | 6 | "code.gitea.io/sdk/gitea" 7 | "github.com/cnoe-io/idpbuilder/api/v1alpha1" 8 | "github.com/cnoe-io/idpbuilder/pkg/util" 9 | "github.com/google/go-github/v61/github" 10 | ) 11 | 12 | type GiteaClient interface { 13 | CreateAccessToken(option gitea.CreateAccessTokenOption) (*gitea.AccessToken, *gitea.Response, error) 14 | CreateOrg(opt gitea.CreateOrgOption) (*gitea.Organization, *gitea.Response, error) 15 | CreateRepo(opt gitea.CreateRepoOption) (*gitea.Repository, *gitea.Response, error) 16 | DeleteOrg(orgname string) (*gitea.Response, error) 17 | DeleteRepo(owner, repo string) (*gitea.Response, error) 18 | GetOrg(orgname string) (*gitea.Organization, *gitea.Response, error) 19 | GetRepo(owner, reponame string) (*gitea.Repository, *gitea.Response, error) 20 | SetBasicAuth(username, password string) 21 | SetContext(ctx context.Context) 22 | } 23 | 24 | type gitHubClient interface { 25 | getRepo(ctx context.Context, owner, repo string) (*github.Repository, *github.Response, error) 26 | createRepo(ctx context.Context, owner string, req *github.Repository) (*github.Repository, *github.Response, error) 27 | setToken(token string) error 28 | } 29 | 30 | type repoInfo struct { 31 | name string 32 | cloneUrl string 33 | internalGitRepositoryUrl string 34 | fullName string 35 | } 36 | 37 | type gitProviderCredentials struct { 38 | username string 39 | password string 40 | accessToken string 41 | } 42 | 43 | type gitProvider interface { 44 | createRepository(ctx context.Context, repo *v1alpha1.GitRepository) (repoInfo, error) 45 | getProviderCredentials(ctx context.Context, repo *v1alpha1.GitRepository) (gitProviderCredentials, error) 46 | getRepository(ctx context.Context, repo *v1alpha1.GitRepository) (repoInfo, error) 47 | setProviderCredentials(ctx context.Context, repo *v1alpha1.GitRepository, creds gitProviderCredentials) error 48 | updateRepoContent(ctx context.Context, repo *v1alpha1.GitRepository, repoInfo repoInfo, creds gitProviderCredentials, tmpDir string, repoMap *util.RepoMap) error 49 | } 50 | -------------------------------------------------------------------------------- /pkg/k8s/util_test.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "bytes" 5 | "embed" 6 | "os" 7 | "testing" 8 | 9 | "github.com/cnoe-io/idpbuilder/api/v1alpha1" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | //go:embed test-resources/* 14 | var testDataFS embed.FS 15 | 16 | func TestBuildCustomizedManifests(t *testing.T) { 17 | cases := map[string]struct { 18 | fsPath string 19 | filePath string 20 | expectedFilepath string 21 | }{ 22 | "argocd": { 23 | fsPath: "test-resources/input/argocd", 24 | filePath: "test-resources/input/argocd-cm.yaml", 25 | expectedFilepath: "test-resources/output/argocd/install.yaml", 26 | }, 27 | "nginx": { 28 | fsPath: "test-resources/input/nginx", 29 | filePath: "test-resources/input/extra.yaml", 30 | expectedFilepath: "test-resources/output/nginx/install.yaml", 31 | }, 32 | "nginx-template": { 33 | fsPath: "test-resources/input/nginx", 34 | filePath: "test-resources/input/extra.yaml.tmpl", 35 | expectedFilepath: "test-resources/output/nginx/install-tmpl.yaml", 36 | }, 37 | } 38 | 39 | for key := range cases { 40 | c := cases[key] 41 | b, err := BuildCustomizedManifests(c.filePath, c.fsPath, testDataFS, GetScheme(), v1alpha1.BuildCustomizationSpec{ 42 | Protocol: "http", 43 | Host: "cnoe.localtest.me", 44 | IngressHost: "localhost", 45 | Port: "8443", 46 | UsePathRouting: false, 47 | }) 48 | if err != nil { 49 | t.Fatalf("failed %s: %v", key, err) 50 | } 51 | 52 | expected, _ := os.ReadFile(c.expectedFilepath) 53 | expectedYamls := bytes.Split(expected, []byte{'-', '-', '-'}) 54 | testYamls := make([][]byte, 0, 10) 55 | 56 | for f := range b { 57 | y := bytes.Split(b[f], []byte{'-', '-', '-'}) 58 | testYamls = append(testYamls, y...) 59 | } 60 | 61 | if len(expectedYamls) != len(testYamls) { 62 | t.Fatalf("failed %s: number of yaml objects do not match", key) 63 | } 64 | 65 | for i := 0; i < len(expectedYamls); i++ { 66 | ok := assert.YAMLEq(t, string(expectedYamls[i]), string(testYamls[i])) 67 | if !ok { 68 | t.Fatalf("failed %s", key) 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /pkg/build/coredns.go: -------------------------------------------------------------------------------- 1 | package build 2 | 3 | import ( 4 | "context" 5 | "embed" 6 | "fmt" 7 | 8 | "github.com/cnoe-io/idpbuilder/api/v1alpha1" 9 | "github.com/cnoe-io/idpbuilder/pkg/k8s" 10 | appsv1 "k8s.io/api/apps/v1" 11 | corev1 "k8s.io/api/core/v1" 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | "k8s.io/apimachinery/pkg/runtime" 14 | "sigs.k8s.io/controller-runtime/pkg/client" 15 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 16 | ) 17 | 18 | const ( 19 | coreDNSTemplatePath = "templates/coredns" 20 | ) 21 | 22 | //go:embed templates 23 | var templates embed.FS 24 | 25 | func setupCoreDNS(ctx context.Context, kubeClient client.Client, scheme *runtime.Scheme, templateData v1alpha1.BuildCustomizationSpec) error { 26 | checkCM := &corev1.ConfigMap{ 27 | ObjectMeta: metav1.ObjectMeta{ 28 | Name: "coredns-conf-default", 29 | Namespace: "kube-system", 30 | }, 31 | } 32 | err := kubeClient.Get(ctx, client.ObjectKeyFromObject(checkCM), checkCM) 33 | if err == nil { 34 | return nil 35 | } 36 | 37 | objs, err := k8s.BuildCustomizedObjects("", coreDNSTemplatePath, templates, scheme, templateData) 38 | if err != nil { 39 | return fmt.Errorf("rendering embedded coredns files: %w", err) 40 | } 41 | 42 | for i := range objs { 43 | obj := objs[i] 44 | switch t := obj.(type) { 45 | case *appsv1.Deployment: 46 | dep := &appsv1.Deployment{ 47 | ObjectMeta: metav1.ObjectMeta{ 48 | Name: t.Name, 49 | Namespace: t.Namespace, 50 | }, 51 | } 52 | _, err = controllerutil.CreateOrUpdate(ctx, kubeClient, dep, func() error { 53 | dep.Spec = t.Spec 54 | return nil 55 | }) 56 | if err != nil { 57 | return fmt.Errorf("creating/updating deployment: %w", err) 58 | } 59 | case *corev1.ConfigMap: 60 | cm := &corev1.ConfigMap{ 61 | ObjectMeta: metav1.ObjectMeta{ 62 | Name: t.Name, 63 | Namespace: t.Namespace, 64 | }, 65 | } 66 | _, err = controllerutil.CreateOrUpdate(ctx, kubeClient, cm, func() error { 67 | cm.Data = t.Data 68 | return nil 69 | }) 70 | if err != nil { 71 | return fmt.Errorf("creating/updating configmap: %w", err) 72 | } 73 | } 74 | } 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /pkg/util/url_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestURLParse(t *testing.T) { 11 | 12 | type expect struct { 13 | cloneUrl string 14 | path string 15 | ref string 16 | submodule bool 17 | timeout time.Duration 18 | err bool 19 | } 20 | 21 | type testCase struct { 22 | expect expect 23 | input string 24 | } 25 | 26 | cases := []testCase{ 27 | { 28 | input: "https://github.com/kubernetes-sigs/kustomize//examples/multibases/dev/?timeout=120&ref=v3.3.1", 29 | expect: expect{ 30 | cloneUrl: "https://github.com/kubernetes-sigs/kustomize", 31 | path: "examples/multibases/dev", 32 | ref: "v3.3.1", 33 | submodule: true, 34 | timeout: 120 * time.Second, 35 | }, 36 | }, 37 | { 38 | input: "git@github.com:owner/repo//examples?timeout=120&version=v3.3.1", 39 | expect: expect{ 40 | cloneUrl: "git@github.com:owner/repo", 41 | path: "examples", 42 | ref: "v3.3.1", 43 | submodule: true, 44 | timeout: 120 * time.Second, 45 | }, 46 | }, 47 | { 48 | input: "https:// /(@kubernetes-sigs/kustomize//examples/multibases/dev/?timeout=120&ref=v3.3.1", 49 | expect: expect{ 50 | err: true, 51 | }, 52 | }, 53 | { 54 | input: "https://my.github.com/kubernetes-sigs/kustomize//examples/multibases/dev/?version=v3.3.1&submodules=false&timeout=1s", 55 | expect: expect{ 56 | cloneUrl: "https://my.github.com/kubernetes-sigs/kustomize", 57 | path: "examples/multibases/dev", 58 | ref: "v3.3.1", 59 | submodule: false, 60 | timeout: 1 * time.Second, 61 | }, 62 | }, 63 | } 64 | 65 | for i := range cases { 66 | c := cases[i] 67 | 68 | r, err := NewKustomizeRemote(c.input) 69 | if err != nil { 70 | if !c.expect.err { 71 | assert.Fail(t, err.Error()) 72 | } else { 73 | continue 74 | } 75 | } 76 | assert.Equal(t, c.expect.path, r.Path()) 77 | assert.Equal(t, c.expect.cloneUrl, r.CloneUrl()) 78 | assert.Equal(t, c.expect.timeout, r.Timeout) 79 | assert.Equal(t, c.expect.ref, r.Ref) 80 | assert.Equal(t, c.expect.submodule, r.Submodules) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /pkg/controllers/run.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/cnoe-io/idpbuilder/api/v1alpha1" 7 | "github.com/cnoe-io/idpbuilder/pkg/controllers/custompackage" 8 | "github.com/cnoe-io/idpbuilder/pkg/util" 9 | 10 | "github.com/cnoe-io/idpbuilder/pkg/controllers/gitrepository" 11 | "github.com/cnoe-io/idpbuilder/pkg/controllers/localbuild" 12 | "sigs.k8s.io/controller-runtime/pkg/log" 13 | "sigs.k8s.io/controller-runtime/pkg/manager" 14 | ) 15 | 16 | func RunControllers( 17 | ctx context.Context, 18 | mgr manager.Manager, 19 | exitCh chan error, 20 | ctxCancel context.CancelFunc, 21 | exitOnSync bool, 22 | cfg v1alpha1.BuildCustomizationSpec, 23 | tmpDir string, 24 | ) error { 25 | logger := log.FromContext(ctx) 26 | 27 | repoMap := util.NewRepoLock() 28 | 29 | // Run Localbuild controller 30 | if err := (&localbuild.LocalbuildReconciler{ 31 | Client: mgr.GetClient(), 32 | Scheme: mgr.GetScheme(), 33 | ExitOnSync: exitOnSync, 34 | CancelFunc: ctxCancel, 35 | Config: cfg, 36 | TempDir: tmpDir, 37 | RepoMap: repoMap, 38 | }).SetupWithManager(mgr); err != nil { 39 | logger.Error(err, "unable to create localbuild controller") 40 | return err 41 | } 42 | 43 | err := (&gitrepository.RepositoryReconciler{ 44 | Client: mgr.GetClient(), 45 | Scheme: mgr.GetScheme(), 46 | Recorder: mgr.GetEventRecorderFor("gitrepository-controller"), 47 | Config: cfg, 48 | GitProviderFunc: gitrepository.GetGitProvider, 49 | TempDir: tmpDir, 50 | RepoMap: repoMap, 51 | }).SetupWithManager(mgr, nil) 52 | if err != nil { 53 | logger.Error(err, "unable to create repo controller") 54 | } 55 | 56 | err = (&custompackage.Reconciler{ 57 | Client: mgr.GetClient(), 58 | Scheme: mgr.GetScheme(), 59 | Recorder: mgr.GetEventRecorderFor("custompackage-controller"), 60 | TempDir: tmpDir, 61 | RepoMap: repoMap, 62 | }).SetupWithManager(mgr) 63 | if err != nil { 64 | logger.Error(err, "unable to create custom package controller") 65 | } 66 | // Start our manager in another goroutine 67 | logger.V(1).Info("starting manager") 68 | 69 | go func() { 70 | exitCh <- mgr.Start(ctx) 71 | close(exitCh) 72 | }() 73 | 74 | return nil 75 | } 76 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | project_name: idpbuilder 3 | 4 | before: 5 | hooks: 6 | - go mod tidy 7 | release: 8 | # Mark nightly build as prerelease based on tag 9 | prerelease: auto 10 | 11 | builds: 12 | - env: 13 | - CGO_ENABLED=0 14 | goos: 15 | - linux 16 | - darwin 17 | goarch: 18 | - amd64 19 | - arm64 20 | ldflags: 21 | - -X github.com/cnoe-io/idpbuilder/pkg/cmd/version.idpbuilderVersion={{ .Version }} 22 | - -X github.com/cnoe-io/idpbuilder/pkg/cmd/version.gitCommit={{ .FullCommit }} 23 | - -X github.com/cnoe-io/idpbuilder/pkg/cmd/version.buildDate={{ .CommitDate }} 24 | - -w 25 | - -s 26 | binary: idpbuilder 27 | ignore: 28 | - goos: linux 29 | goarch: '386' 30 | brews: 31 | # For non version installations 32 | - name: "idpbuilder" 33 | homepage: "https://cnoe.io" 34 | skip_upload: "{{ contains .Tag \"nightly\" }}" # Don't upload this formula if the build is nightly 35 | repository: 36 | owner: cnoe-io 37 | name: homebrew-tap 38 | token: "{{ .Env.HOMEBREW_TOKEN }}" 39 | commit_author: 40 | name: "CNOEAutomation" 41 | email: "noreply@cnoe.io" 42 | directory: Formula 43 | install: | 44 | bin.install "idpbuilder" 45 | test: | 46 | system "#{bin}/idpbuilder version" 47 | # For versioned and nightly installations 48 | - name: "{{ if contains .Tag \"nightly\" }}idpbuilder-nightly{{ else }}idpbuilder@{{ .Major }}.{{ .Minor }}.{{ .Patch }}{{ end }}" 49 | homepage: "https://cnoe.io" 50 | repository: 51 | owner: cnoe-io 52 | name: homebrew-tap 53 | token: "{{ .Env.HOMEBREW_TOKEN }}" 54 | commit_author: 55 | name: "CNOEAutomation" 56 | email: "noreply@cnoe.io" 57 | directory: Formula 58 | install: | 59 | bin.install "idpbuilder" 60 | test: | 61 | system "#{bin}/idpbuilder version" 62 | archives: 63 | - format: tar.gz 64 | name_template: >- 65 | {{ .ProjectName }}-{{ .Os }}-{{ .Arch }} 66 | checksum: 67 | name_template: 'checksums.txt' 68 | snapshot: 69 | version_template: "{{ incpatch .Version }}-next" 70 | changelog: 71 | sort: asc 72 | filters: 73 | exclude: 74 | - '^docs:' 75 | - '^test:' 76 | use: github 77 | format: "{{.SHA}}: {{.Message}} [{{.AuthorName}}](@{{.AuthorUsername}})" 78 | groups: 79 | - title: "✨ Features" 80 | regexp: '^.*?feat(\([[:word:]]+\))??!?:.+$' 81 | order: 0 82 | - title: "🐛 Bug fixes" 83 | regexp: '^.*?(fix|bug)(\([[:word:]]+\))??!?:.+$' 84 | order: 1 85 | - title: "🔧 Others" 86 | order: 999 87 | -------------------------------------------------------------------------------- /pkg/cmd/helpers/validation.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/cnoe-io/idpbuilder/pkg/util" 9 | "sigs.k8s.io/kustomize/kyaml/kio" 10 | ) 11 | 12 | func ValidateKubernetesYamlFile(absPath string) error { 13 | if !filepath.IsAbs(absPath) { 14 | return fmt.Errorf("given path is not an absolute path %s", absPath) 15 | } 16 | b, err := os.ReadFile(absPath) 17 | if err != nil { 18 | return fmt.Errorf("failed reading file: %s, err: %w", absPath, err) 19 | } 20 | n, err := kio.FromBytes(b) 21 | if err != nil { 22 | return fmt.Errorf("failed parsing file as kubernetes manifests file: %s, err: %w", absPath, err) 23 | } 24 | 25 | for i := range n { 26 | obj := n[i] 27 | if obj.IsNilOrEmpty() { 28 | return fmt.Errorf("given file %s contains an invalid kubernetes manifest", absPath) 29 | } 30 | if obj.GetKind() == "" || obj.GetApiVersion() == "" { 31 | return fmt.Errorf("given file %s contains an invalid kubernetes manifest", absPath) 32 | } 33 | } 34 | 35 | return nil 36 | } 37 | 38 | func ParsePackageStrings(pkgStrings []string) ([]string, []string, []string, error) { 39 | remote, files, dirs := make([]string, 0, 2), make([]string, 0, 2), make([]string, 0, 2) 40 | for i := range pkgStrings { 41 | loc := pkgStrings[i] 42 | _, err := util.NewKustomizeRemote(loc) 43 | if err == nil { 44 | remote = append(remote, loc) 45 | continue 46 | } 47 | 48 | absPath, err := getAbsPath(loc, true) 49 | if err == nil { 50 | dirs = append(dirs, absPath) 51 | continue 52 | } 53 | 54 | absPath, err = getAbsPath(loc, false) 55 | if err == nil { 56 | files = append(files, absPath) 57 | continue 58 | } 59 | 60 | return nil, nil, nil, err 61 | } 62 | 63 | return remote, files, dirs, nil 64 | } 65 | 66 | func getAbsPath(path string, isDir bool) (string, error) { 67 | absPath, err := filepath.Abs(path) 68 | if err != nil { 69 | return "", fmt.Errorf("failed to validate path %s : %w", path, err) 70 | } 71 | f, err := os.Stat(absPath) 72 | if err != nil { 73 | return "", fmt.Errorf("failed to validate path %s : %w", absPath, err) 74 | } 75 | 76 | if isDir && !f.IsDir() { 77 | return "", fmt.Errorf("given path is not a directory. %s", absPath) 78 | } 79 | 80 | if !isDir && !f.Mode().IsRegular() { 81 | return "", fmt.Errorf("given path is not a file. %s", absPath) 82 | } 83 | return absPath, nil 84 | } 85 | 86 | func GetAbsFilePaths(paths []string, isDir bool) ([]string, error) { 87 | out := make([]string, len(paths)) 88 | for i := range paths { 89 | absPath, err := getAbsPath(paths[i], isDir) 90 | if err != nil { 91 | return nil, err 92 | } 93 | out[i] = absPath 94 | } 95 | return out, nil 96 | } 97 | -------------------------------------------------------------------------------- /pkg/cmd/helpers/validation_test.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestValidateKubernetesYaml(t *testing.T) { 12 | cwd, err := os.Getwd() 13 | if err != nil { 14 | t.Fatalf("could not get current working directory") 15 | } 16 | 17 | cases := map[string]struct { 18 | expectErr bool 19 | inputPath string 20 | }{ 21 | "invalidPath": {expectErr: true, inputPath: fmt.Sprintf("%s/invalid/path", cwd)}, 22 | "notAbs": {expectErr: true, inputPath: fmt.Sprintf("invalid/path")}, 23 | "valid": {expectErr: false, inputPath: fmt.Sprintf("%s/test-data/valid.yaml", cwd)}, 24 | "notYaml": {expectErr: true, inputPath: fmt.Sprintf("%s/test-data/notyaml.yaml", cwd)}, 25 | "notk8s": {expectErr: true, inputPath: fmt.Sprintf("%s/test-data/notk8s.yaml", cwd)}, 26 | } 27 | 28 | for k := range cases { 29 | cErr := ValidateKubernetesYamlFile(cases[k].inputPath) 30 | if cases[k].expectErr && cErr == nil { 31 | t.Fatalf("%s expected error but did not receive error", k) 32 | } 33 | if !cases[k].expectErr && cErr != nil { 34 | t.Fatalf("%s did not expect error but received error", k) 35 | } 36 | } 37 | } 38 | 39 | func TestParsePackageStrings(t *testing.T) { 40 | cases := map[string]struct { 41 | expectErr bool 42 | inputPaths []string 43 | remote int 44 | files int 45 | dirs int 46 | }{ 47 | "allDirs": {expectErr: false, inputPaths: []string{"test-data", "."}, remote: 0, files: 0, dirs: 2}, 48 | "allFiles": {expectErr: false, inputPaths: []string{"test-data/valid.yaml"}, remote: 0, files: 1, dirs: 0}, 49 | "allRemote": {expectErr: false, inputPaths: []string{ 50 | "https://github.com/kubernetes-sigs/kustomize//examples/multibases/dev/?timeout=120&ref=v3.3.1", 51 | "git@github.com:owner/repo//examples", 52 | }, remote: 2, files: 0, dirs: 0}, 53 | "mix": {expectErr: false, inputPaths: []string{ 54 | "https://github.com/kubernetes-sigs/kustomize//examples/multibases/dev/?timeout=120&ref=v3.3.1", 55 | "test-data", 56 | "test-data/valid.yaml", 57 | }, remote: 1, files: 1, dirs: 1}, 58 | "invalidLocalPath": {expectErr: true, inputPaths: []string{ 59 | "does-not-exist", 60 | }, remote: 0, files: 0, dirs: 0}, 61 | "invalidRemotePath": {expectErr: true, inputPaths: []string{ 62 | "https:// github.com/kubernetes-sigs/kustomize//examples", 63 | }, remote: 0, files: 0, dirs: 0}, 64 | } 65 | 66 | for k := range cases { 67 | c := cases[k] 68 | remote, files, dirs, err := ParsePackageStrings(c.inputPaths) 69 | if cases[k].expectErr { 70 | assert.NotNil(t, err) 71 | } else { 72 | assert.Nil(t, err) 73 | } 74 | assert.Equal(t, c.remote, len(remote)) 75 | assert.Equal(t, c.files, len(files)) 76 | assert.Equal(t, c.dirs, len(dirs)) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /pkg/util/files/files.go: -------------------------------------------------------------------------------- 1 | package files 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "text/template" 11 | ) 12 | 13 | func CopyDirectory(scrDir, dest string) error { 14 | entries, err := os.ReadDir(scrDir) 15 | if err != nil { 16 | return err 17 | } 18 | for _, entry := range entries { 19 | sourcePath := filepath.Join(scrDir, entry.Name()) 20 | destPath := filepath.Join(dest, entry.Name()) 21 | 22 | fileInfo, err := os.Stat(sourcePath) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | switch fileInfo.Mode() & os.ModeType { 28 | case os.ModeDir: 29 | if err := CreateIfNotExists(destPath, 0755); err != nil { 30 | return err 31 | } 32 | if err := CopyDirectory(sourcePath, destPath); err != nil { 33 | return err 34 | } 35 | case os.ModeSymlink: 36 | continue 37 | default: 38 | if err := Copy(sourcePath, destPath); err != nil { 39 | return err 40 | } 41 | } 42 | 43 | fInfo, err := entry.Info() 44 | if err != nil { 45 | return err 46 | } 47 | if err := os.Chmod(destPath, fInfo.Mode()); err != nil { 48 | return err 49 | } 50 | } 51 | return nil 52 | } 53 | 54 | func Copy(srcFile, dstFile string) error { 55 | out, err := os.Create(dstFile) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | defer out.Close() 61 | 62 | in, err := os.Open(srcFile) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | defer in.Close() 68 | 69 | _, err = io.Copy(out, in) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | return nil 75 | } 76 | 77 | func Exists(filePath string) bool { 78 | if _, err := os.Stat(filePath); os.IsNotExist(err) { 79 | return false 80 | } 81 | 82 | return true 83 | } 84 | 85 | func CreateIfNotExists(dir string, perm os.FileMode) error { 86 | if Exists(dir) { 87 | return nil 88 | } 89 | 90 | if err := os.MkdirAll(dir, perm); err != nil { 91 | return fmt.Errorf("failed to create directory: '%s', error: '%s'", dir, err.Error()) 92 | } 93 | 94 | return nil 95 | } 96 | 97 | func ApplyTemplate(in []byte, templateData any) ([]byte, error) { 98 | funcMap := template.FuncMap{ 99 | "indentNewLines": templateIndentNewlines, 100 | } 101 | t, err := template.New("template").Funcs(funcMap).Parse(string(in)) 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | // Execute the template with the file content and write the output to the destination file 107 | ret := bytes.Buffer{} 108 | err = t.Execute(&ret, templateData) 109 | if err != nil { 110 | return nil, err 111 | } 112 | 113 | return ret.Bytes(), nil 114 | } 115 | 116 | // indent given string with given number of spaces whenever a newline symbol is found. 117 | func templateIndentNewlines(n int, val string) string { 118 | return strings.Replace(val, "\n", "\n"+strings.Repeat(" ", n), -1) 119 | } 120 | -------------------------------------------------------------------------------- /pkg/util/fs/fs.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/cnoe-io/idpbuilder/pkg/util/files" 7 | "io" 8 | "io/fs" 9 | "os" 10 | "path" 11 | "path/filepath" 12 | ) 13 | 14 | type FS interface { 15 | ReadDir(name string) ([]fs.DirEntry, error) 16 | ReadFile(name string) ([]byte, error) 17 | } 18 | 19 | func ConvertFSToBytes(inFS FS, name string, templateData any) ([][]byte, error) { 20 | d, err := inFS.ReadDir(name) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | var rawResources [][]byte 26 | 27 | for _, f := range d { 28 | rawResource, err := inFS.ReadFile(path.Join(name, f.Name())) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | if returnedRawResource, err := files.ApplyTemplate(rawResource, templateData); err == nil { 34 | rawResources = append(rawResources, returnedRawResource) 35 | } else { 36 | return nil, err 37 | } 38 | } 39 | 40 | return rawResources, nil 41 | } 42 | 43 | func CopyFile(src fs.File, dest string) error { 44 | srcStat, srcStatErr := src.Stat() 45 | if srcStatErr != nil { 46 | return srcStatErr 47 | } 48 | 49 | destFn := filepath.Join(dest, srcStat.Name()) 50 | 51 | destf, destErr := os.OpenFile( 52 | destFn, 53 | os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 54 | srcStat.Mode(), 55 | ) 56 | if destErr != nil { 57 | return fmt.Errorf("opening a file for writing: %w", destErr) 58 | } 59 | 60 | _, err := io.Copy(destf, src) 61 | if err != nil { 62 | return fmt.Errorf("copying %s to %s", srcStat.Name(), destFn) 63 | } 64 | 65 | return destf.Close() 66 | } 67 | 68 | func CopyDir(src fs.FS, dest string) error { 69 | ents, err := fs.ReadDir(src, ".") 70 | if err != nil { 71 | return fmt.Errorf("reading src: %w", err) 72 | } 73 | 74 | for _, sdent := range ents { 75 | info, err := sdent.Info() 76 | if err != nil { 77 | return fmt.Errorf("reading file info: %v", err) 78 | } 79 | switch { 80 | case info.IsDir(): 81 | subDest := filepath.Join(dest, sdent.Name()) 82 | 83 | if err := os.Mkdir(subDest, 0700); err != nil { 84 | return fmt.Errorf("mkdir on %s: %w", subDest, err) 85 | } 86 | 87 | subFS, err := fs.Sub(src, sdent.Name()) 88 | if err != nil { 89 | return fmt.Errorf("reading the sub directory: %w", err) 90 | } 91 | 92 | if err := CopyDir(subFS, subDest); err != nil { 93 | return err 94 | } 95 | case info.Mode().IsRegular(): 96 | srcf, err := src.Open(info.Name()) 97 | if err != nil { 98 | return err 99 | } 100 | if err := CopyFile(srcf, dest); err != nil { 101 | return err 102 | } 103 | } 104 | } 105 | 106 | return nil 107 | } 108 | 109 | func WriteFS(src fs.FS, dest string) error { 110 | destInfo, destErr := os.Lstat(dest) 111 | if destErr != nil { 112 | return destErr 113 | } 114 | 115 | if !destInfo.IsDir() { 116 | return errors.New("the destination must be a directory") 117 | } 118 | 119 | return CopyDir(src, dest) 120 | } 121 | -------------------------------------------------------------------------------- /api/v1alpha1/custom_package_types.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | const ( 8 | CNOEURIScheme = "cnoe://" 9 | ) 10 | 11 | // +kubebuilder:object:root=true 12 | // +kubebuilder:subresource:status 13 | type CustomPackage struct { 14 | metav1.TypeMeta `json:",inline"` 15 | metav1.ObjectMeta `json:"metadata,omitempty"` 16 | 17 | Spec CustomPackageSpec `json:"spec,omitempty"` 18 | Status CustomPackageStatus `json:"status,omitempty"` 19 | } 20 | 21 | // +kubebuilder:object:root=true 22 | type CustomPackageList struct { 23 | metav1.TypeMeta `json:",inline"` 24 | metav1.ListMeta `json:"metadata,omitempty"` 25 | Items []CustomPackage `json:"items"` 26 | } 27 | 28 | // CustomPackageSpec controls the installation of the custom applications. 29 | type CustomPackageSpec struct { 30 | ArgoCD ArgoCDPackageSpec `json:"argoCD,omitempty"` 31 | // GitServerURL specifies the base URL for the git server for API calls. 32 | // for example, https://gitea.cnoe.localtest.me:8443 33 | GitServerURL string `json:"gitServerURL"` 34 | GitServerAuthSecretRef SecretReference `json:"gitServerAuthSecretRef"` 35 | // InternalGitServeURL specifies the base URL for the git server accessible within the cluster. 36 | // for example, http://my-gitea-http.gitea.svc.cluster.local:3000 37 | InternalGitServeURL string `json:"internalGitServeURL"` 38 | RemoteRepository RemoteRepositorySpec `json:"remoteRepository"` 39 | // Replicate specifies whether to replicate remote or local contents to the local gitea server. 40 | // +kubebuilder:default:=false 41 | Replicate bool `json:"replicate"` 42 | } 43 | 44 | // RemoteRepositorySpec specifies information about remote repositories. 45 | type RemoteRepositorySpec struct { 46 | CloneSubmodules bool `json:"cloneSubmodules"` 47 | Path string `json:"path"` 48 | // Url specifies the url to the repository containing the ArgoCD application file 49 | Url string `json:"url"` 50 | // Ref specifies the specific ref supported by git fetch 51 | Ref string `json:"ref"` 52 | } 53 | 54 | type ArgoCDPackageSpec struct { 55 | // ApplicationFile specifies the absolute path to the ArgoCD application file 56 | ApplicationFile string `json:"applicationFile"` 57 | Name string `json:"name"` 58 | Namespace string `json:"namespace"` 59 | // +kubebuilder:validation:Enum:=Application;ApplicationSet 60 | Type string `json:"type"` 61 | } 62 | 63 | type CustomPackageStatus struct { 64 | // A Custom package is considered synced when the in-cluster repository url is set as the repository URL 65 | // This only applies for a package that references local directories 66 | Synced bool `json:"synced,omitempty"` 67 | GitRepositoryRefs []ObjectRef `json:"gitRepositoryRefs,omitempty"` 68 | } 69 | 70 | type ObjectRef struct { 71 | APIVersion string `json:"apiVersion,omitempty"` 72 | Name string `json:"name,omitempty"` 73 | Namespace string `json:"namespace,omitempty"` 74 | Kind string `json:"kind,omitempty"` 75 | UID string `json:"uid,omitempty"` 76 | } 77 | -------------------------------------------------------------------------------- /.github/workflows/nightly.yaml: -------------------------------------------------------------------------------- 1 | name: nightly 2 | on: 3 | # This can be used to automatically publish nightlies at UTC nighttime 4 | schedule: 5 | - cron: '0 7 * * *' # run at 7 AM UTC 6 | # This can be used to allow manually triggering nightlies from the web interface 7 | workflow_dispatch: 8 | 9 | jobs: 10 | nightly: 11 | runs-on: ubuntu-22.04 12 | steps: 13 | - name: Generate a token 14 | id: generate-token 15 | uses: actions/create-github-app-token@v1 16 | with: 17 | app-id: ${{ vars.CNOE_GH_WORKFLOW_TOKEN_APP_ID }} 18 | private-key: ${{ secrets.CNOE_GH_WORKFLOW_TOKEN_PRIVATE_KEY }} 19 | - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 20 | with: 21 | token: ${{ steps.generate-token.outputs.token }} 22 | fetch-depth: 0 23 | 24 | - run: git fetch --force --tags 25 | - 26 | name: 'Push new tag' 27 | run: | 28 | git config user.name "${GITHUB_ACTOR}" 29 | git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" 30 | 31 | # A previous release was created using a lightweight tag 32 | # git describe by default includes only annotated tags 33 | # git describe --tags includes lightweight tags as well 34 | DESCRIBE=`git tag -l --sort=-v:refname | grep -v nightly | head -n 1` 35 | MAJOR_VERSION=`echo $DESCRIBE | awk '{split($0,a,"."); print a[1]}'` 36 | MINOR_VERSION=`echo $DESCRIBE | awk '{split($0,a,"."); print a[2]}'` 37 | MINOR_VERSION="$((${MINOR_VERSION} + 1))" 38 | TAG="${MAJOR_VERSION}.${MINOR_VERSION}.0-nightly.$(date +'%Y%m%d')" 39 | git tag -a $TAG -m "$TAG: nightly build" 40 | git push origin $TAG 41 | - name: Find previous nightly 42 | run: | 43 | prev_tag=$(git tag | grep "nightly" | sort -r --version-sort | head -n 2 | tail -n 1) 44 | echo "PREVIOUS_NIGHTLY_TAG=$prev_tag" >> $GITHUB_ENV 45 | git push --delete origin $prev_tag 46 | - name: 'Clean up nightly releases' 47 | uses: actions/github-script@v7 48 | with: 49 | github-token: ${{ steps.generate-token.outputs.token }} 50 | script: | 51 | const latestRelease = await github.rest.repos.getReleaseByTag({ 52 | owner: "${{ github.repository_owner }}", 53 | repo: "${{ github.event.repository.name }}", 54 | tag: "${{ env.PREVIOUS_NIGHTLY_TAG }}" 55 | }); 56 | console.log(`Release ${latestRelease}`); 57 | if (latestRelease && latestRelease.data && latestRelease.data.id) { 58 | await github.rest.repos.deleteRelease({ 59 | owner: "${{ github.repository_owner }}", 60 | repo: "${{ github.event.repository.name }}", 61 | release_id: latestRelease.data.id, 62 | }); 63 | console.log(`Release id ${latestRelease.data.id} has been deleted.`); 64 | } else { 65 | console.log("No latest release found or failed to retrieve it."); 66 | } 67 | -------------------------------------------------------------------------------- /pkg/build/tls_test.go: -------------------------------------------------------------------------------- 1 | package build 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "encoding/pem" 8 | "testing" 9 | 10 | "github.com/cnoe-io/idpbuilder/globals" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/mock" 13 | corev1 "k8s.io/api/core/v1" 14 | k8serrors "k8s.io/apimachinery/pkg/api/errors" 15 | "k8s.io/apimachinery/pkg/runtime/schema" 16 | "sigs.k8s.io/controller-runtime/pkg/client" 17 | ) 18 | 19 | type fakeKubeClient struct { 20 | mock.Mock 21 | client.Client 22 | } 23 | 24 | func (f *fakeKubeClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { 25 | args := f.Called(ctx, key, obj, opts) 26 | return args.Error(0) 27 | } 28 | 29 | func (f *fakeKubeClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error { 30 | args := f.Called(ctx, obj, opts) 31 | return args.Error(0) 32 | } 33 | 34 | func TestCreateSelfSignedCertificate(t *testing.T) { 35 | sans := []string{"cnoe.io", "*.cnoe.io"} 36 | c, k, err := createSelfSignedCertificate(sans) 37 | assert.NoError(t, err) 38 | _, err = tls.X509KeyPair(c, k) 39 | assert.NoError(t, err) 40 | 41 | block, _ := pem.Decode(c) 42 | assert.Equal(t, "CERTIFICATE", block.Type) 43 | cert, err := x509.ParseCertificate(block.Bytes) 44 | assert.NoError(t, err) 45 | 46 | assert.Equal(t, 2, len(cert.DNSNames)) 47 | expected := map[string]struct{}{ 48 | "cnoe.io": {}, 49 | "*.cnoe.io": {}, 50 | } 51 | 52 | for _, s := range cert.DNSNames { 53 | _, ok := expected[s] 54 | if ok { 55 | delete(expected, s) 56 | } else { 57 | t.Fatalf("unexpected key %s found", s) 58 | } 59 | } 60 | assert.Equal(t, 0, len(expected)) 61 | } 62 | 63 | func TestGetOrCreateIngressCertificateAndKey(t *testing.T) { 64 | ctx := context.Background() 65 | fClient := new(fakeKubeClient) 66 | fClient.On("Get", ctx, client.ObjectKey{Name: globals.SelfSignedCertSecretName, Namespace: globals.NginxNamespace}, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { 67 | arg := args.Get(2).(*corev1.Secret) 68 | d := map[string][]byte{ 69 | corev1.TLSPrivateKeyKey: []byte("abc"), 70 | corev1.TLSCertKey: []byte("abc"), 71 | } 72 | arg.Data = d 73 | }).Return(nil) 74 | 75 | _, _, err := getOrCreateIngressCertificateAndKey(ctx, fClient, globals.SelfSignedCertSecretName, globals.NginxNamespace, []string{globals.DefaultHostName, globals.DefaultSANWildcard}) 76 | assert.NoError(t, err) 77 | fClient.AssertExpectations(t) 78 | 79 | fClient = new(fakeKubeClient) 80 | fClient.On("Get", ctx, client.ObjectKey{Name: globals.SelfSignedCertSecretName, Namespace: globals.NginxNamespace}, mock.Anything, mock.Anything). 81 | Return(k8serrors.NewNotFound(schema.GroupResource{}, "name")) 82 | fClient.On("Create", ctx, mock.Anything, mock.Anything).Return(nil) 83 | 84 | c, k, err := getOrCreateIngressCertificateAndKey(ctx, fClient, globals.SelfSignedCertSecretName, globals.NginxNamespace, []string{globals.DefaultHostName, globals.DefaultSANWildcard}) 85 | assert.NoError(t, err) 86 | _, err = tls.X509KeyPair(c, k) 87 | assert.NoError(t, err) 88 | } 89 | -------------------------------------------------------------------------------- /pkg/cmd/version/root.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "runtime" 7 | 8 | "github.com/spf13/cobra" 9 | "sigs.k8s.io/yaml" 10 | ) 11 | 12 | var ( 13 | // Flags 14 | outputFormat string 15 | ) 16 | 17 | var VersionCmd = &cobra.Command{ 18 | Use: "version", 19 | Short: "Print idpbuilder version and environment info", 20 | Long: "Print idpbulider version and environment info. This is useful in bug reports and CI.", 21 | RunE: version, 22 | } 23 | 24 | func init() { 25 | VersionCmd.Flags().StringVarP(&outputFormat, "output", "o", "", `Print the idpbuilder version information in a given output format. Accepts "wide", "json", and "yaml".`) 26 | } 27 | 28 | var ( 29 | idpbuilderVersion = "unknown" 30 | goVersion = runtime.Version() 31 | goOs = runtime.GOOS 32 | goArch = runtime.GOARCH 33 | gitCommit = "$Format:%H$" // sha1 from git, output of $(git rev-parse HEAD) 34 | buildDate = "1970-01-01T00:00:00Z" // build date in ISO8601 format, output of $(date -u +'%Y-%m-%dT%H:%M:%SZ') 35 | ) 36 | 37 | type idpbuilderInfo struct { 38 | IdpbuilderVersion string `json:"idpbuilderVersion"` 39 | GoVersion string `json:"goVersion"` 40 | GoOs string `json:"goOs"` 41 | GoArch string `json:"goArch"` 42 | GitCommit string `json:"gitCommit"` 43 | BuildDate string `json:"buildDate"` 44 | } 45 | 46 | func version(cmd *cobra.Command, args []string) error { 47 | switch outputFormat { 48 | case "wide": 49 | cmd.Println(fmt.Sprintf("Version: %#v", idpbuilderInfo{ 50 | idpbuilderVersion, 51 | goVersion, 52 | goOs, 53 | goArch, 54 | gitCommit, 55 | buildDate, 56 | })) 57 | case "json": 58 | jsonInfo, err := jsonInfo() 59 | if err != nil { 60 | return err 61 | } 62 | cmd.Println(jsonInfo) 63 | case "yaml": 64 | yamlInfo, err := yamlInfo() 65 | if err != nil { 66 | return err 67 | } 68 | cmd.Println(yamlInfo) 69 | case "": 70 | cmd.Println(fmt.Sprintf("idpbuilder %s %s %s/%s", 71 | idpbuilderVersion, 72 | goVersion, 73 | goOs, 74 | goArch)) 75 | default: 76 | return fmt.Errorf("invalid output format: %s", outputFormat) 77 | } 78 | 79 | return nil 80 | } 81 | 82 | func jsonInfo() (string, error) { 83 | info := idpbuilderInfo{ 84 | IdpbuilderVersion: idpbuilderVersion, 85 | GoVersion: goVersion, 86 | GoOs: goOs, 87 | GoArch: goArch, 88 | GitCommit: gitCommit, 89 | BuildDate: buildDate, 90 | } 91 | bytes, err := json.Marshal(info) 92 | if err != nil { 93 | return "", err 94 | } 95 | return string(bytes), nil 96 | } 97 | 98 | func yamlInfo() (string, error) { 99 | info := idpbuilderInfo{ 100 | IdpbuilderVersion: idpbuilderVersion, 101 | GoVersion: goVersion, 102 | GoOs: goOs, 103 | GoArch: goArch, 104 | GitCommit: gitCommit, 105 | BuildDate: buildDate, 106 | } 107 | bytes, err := yaml.Marshal(info) 108 | if err != nil { 109 | return "", err 110 | } 111 | return string(bytes), nil 112 | } 113 | -------------------------------------------------------------------------------- /pkg/controllers/crd.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | "embed" 6 | "fmt" 7 | "github.com/cnoe-io/idpbuilder/pkg/util/fs" 8 | "time" 9 | 10 | "github.com/cnoe-io/idpbuilder/pkg/k8s" 11 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 12 | apierrors "k8s.io/apimachinery/pkg/api/errors" 13 | "k8s.io/apimachinery/pkg/runtime" 14 | "k8s.io/apimachinery/pkg/types" 15 | "sigs.k8s.io/controller-runtime/pkg/client" 16 | "sigs.k8s.io/controller-runtime/pkg/log" 17 | ) 18 | 19 | //go:embed resources/*.yaml 20 | var crdFS embed.FS 21 | 22 | func getK8sResources(scheme *runtime.Scheme, templateData any) ([]client.Object, error) { 23 | rawResources, err := fs.ConvertFSToBytes(crdFS, "resources", templateData) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | return k8s.ConvertRawResourcesToObjects(scheme, rawResources) 29 | } 30 | 31 | func EnsureCRD(ctx context.Context, scheme *runtime.Scheme, kubeClient client.Client, obj client.Object) error { 32 | logger := log.FromContext(ctx) 33 | 34 | // Check if the CRD already exists 35 | crd, ok := obj.(*apiextensionsv1.CustomResourceDefinition) 36 | if !ok { 37 | return fmt.Errorf("non crd object passed to EnsureCRD: %v", obj) 38 | } 39 | var curCRD apiextensionsv1.CustomResourceDefinition 40 | err := kubeClient.Get( 41 | ctx, 42 | types.NamespacedName{Name: obj.GetName(), Namespace: "default"}, 43 | &curCRD) 44 | 45 | switch { 46 | case apierrors.IsNotFound(err): 47 | if err := kubeClient.Create(ctx, obj); err != nil { 48 | logger.Error(err, "Unable to create CRD", "resource", obj) 49 | return err 50 | } 51 | case err != nil: 52 | logger.Error(err, "Unable to get CRD during initial check", "resource", obj) 53 | return err 54 | default: 55 | crd.SetResourceVersion(curCRD.GetResourceVersion()) 56 | if err = kubeClient.Update(ctx, crd); err != nil { 57 | logger.Error(err, "Updating CRD", "resource", obj) 58 | return err 59 | } 60 | } 61 | 62 | // There is some async work before the CRD actually exists, wait for this 63 | for { 64 | if err := kubeClient.Get( 65 | ctx, 66 | types.NamespacedName{Name: obj.GetName(), Namespace: "default"}, 67 | &curCRD, 68 | ); err != nil { 69 | logger.Error(err, "Failed to get CRD", "crd name", obj.GetName()) 70 | return err 71 | } 72 | crdEstablished := false 73 | for _, cond := range curCRD.Status.Conditions { 74 | if cond.Type == apiextensionsv1.Established { 75 | if cond.Status == apiextensionsv1.ConditionTrue { 76 | crdEstablished = true 77 | } 78 | } 79 | } 80 | if crdEstablished { 81 | break 82 | } else { 83 | logger.V(1).Info("crd not yet established, waiting.", "crd name", obj.GetName()) 84 | } 85 | time.Sleep(time.Duration(time.Duration.Milliseconds(500))) 86 | } 87 | return nil 88 | } 89 | 90 | func EnsureCRDs(ctx context.Context, scheme *runtime.Scheme, kubeClient client.Client, templateData any) error { 91 | installObjs, err := getK8sResources(scheme, templateData) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | for _, obj := range installObjs { 97 | if err = EnsureCRD(ctx, scheme, kubeClient, obj); err != nil { 98 | return err 99 | } 100 | } 101 | 102 | return nil 103 | } 104 | -------------------------------------------------------------------------------- /api/v1alpha1/gitrepository_types.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | const ( 8 | GitProviderGitea = "gitea" 9 | GitProviderGitHub = "github" 10 | GiteaAdminUserName = "giteaAdmin" 11 | SourceTypeLocal = "local" 12 | SourceTypeRemote = "remote" 13 | SourceTypeEmbedded = "embedded" 14 | ) 15 | 16 | type GitRepositorySpec struct { 17 | // +kubebuilder:validation:Optional 18 | Customization PackageCustomization `json:"customization,omitempty"` 19 | // SecretRef is the reference to secret that contain Git server credentials 20 | // +kubebuilder:validation:Optional 21 | SecretRef SecretReference `json:"secretRef"` 22 | Source GitRepositorySource `json:"source,omitempty"` 23 | Provider Provider `json:"provider"` 24 | } 25 | 26 | type GitRepositorySource struct { 27 | // +kubebuilder:validation:Enum:=argocd;gitea;nginx 28 | // +kubebuilder:validation:Optional 29 | EmbeddedAppName string `json:"embeddedAppName,omitempty"` 30 | // Path is the absolute path to directory that contains Kustomize structure or raw manifests. 31 | // This is required when Type is set to local. 32 | // +kubebuilder:validation:Optional 33 | Path string `json:"path"` 34 | RemoteRepository RemoteRepositorySpec `json:"remoteRepository"` 35 | // Type is the source type. 36 | // +kubebuilder:validation:Enum:=local;embedded;remote 37 | // +kubebuilder:default:=embedded 38 | Type string `json:"type"` 39 | } 40 | 41 | type Provider struct { 42 | // +kubebuilder:validation:Enum:=gitea;github 43 | // +kubebuilder:validation:Required 44 | Name string `json:"name"` 45 | // GitURL is the base URL of Git server used for API calls. 46 | // +kubebuilder:validation:Required 47 | // +kubebuilder:validation:Pattern=`^https?:\/\/.+$` 48 | GitURL string `json:"gitURL"` 49 | // InternalGitURL is the base URL of Git server accessible within the cluster only. 50 | InternalGitURL string `json:"internalGitURL"` 51 | OrganizationName string `json:"organizationName"` 52 | } 53 | 54 | type SecretReference struct { 55 | Name string `json:"name"` 56 | Namespace string `json:"namespace"` 57 | } 58 | 59 | type Commit struct { 60 | // Hash is the digest of the most recent commit 61 | // +kubebuilder:validation:Optional 62 | Hash string `json:"hash"` 63 | } 64 | 65 | type GitRepositoryStatus struct { 66 | // LatestCommit is the most recent commit known to the controller 67 | // +kubebuilder:validation:Optional 68 | LatestCommit Commit `json:"commit"` 69 | // ExternalGitRepositoryUrl is the url for the in-cluster repository accessible from local machine. 70 | // +kubebuilder:validation:Optional 71 | ExternalGitRepositoryUrl string `json:"externalGitRepositoryUrl"` 72 | // InternalGitRepositoryUrl is the url for the in-cluster repository accessible within the cluster. 73 | // +kubebuilder:validation:Optional 74 | InternalGitRepositoryUrl string `json:"internalGitRepositoryUrl"` 75 | // Path is the path within the repository that contains the files. 76 | // +kubebuilder:validation:Optional 77 | Path string `json:"path"` 78 | Synced bool `json:"synced"` 79 | } 80 | 81 | // +kubebuilder:object:root=true 82 | // +kubebuilder:subresource:status 83 | type GitRepository struct { 84 | metav1.TypeMeta `json:",inline"` 85 | metav1.ObjectMeta `json:"metadata,omitempty"` 86 | 87 | Spec GitRepositorySpec `json:"spec,omitempty"` 88 | Status GitRepositoryStatus `json:"status,omitempty"` 89 | } 90 | 91 | // +kubebuilder:object:root=true 92 | type GitRepositoryList struct { 93 | metav1.TypeMeta `json:",inline"` 94 | metav1.ListMeta `json:"metadata,omitempty"` 95 | Items []GitRepository `json:"items"` 96 | } 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Codespell][codespell-badge]][codespell-link] 2 | [![E2E][e2e-badge]][e2e-link] 3 | [![Go Report Card][report-badge]][report-link] 4 | [![Commit Activity][commit-activity-badge]][commit-activity-link] 5 | 6 | # IDP Builder 7 | 8 | Internal development platform binary launcher. 9 | 10 | ## About 11 | 12 | Spin up a complete internal developer platform using industry standard technologies like Kubernetes, Argo, and backstage with only Docker required as a dependency. 13 | 14 | This can be useful in several ways: 15 | * Create a single binary which can demonstrate an IDP reference implementation. 16 | * Use within CI to perform integration testing. 17 | * Use as a local development environment for platform engineers. 18 | 19 | ## Installation 20 | ### Using [Homebrew](https://brew.sh) 21 | + Stable Version 22 | 23 | ```bash 24 | brew install cnoe-io/tap/idpbuilder 25 | ``` 26 | + Specific Stable Version 27 | 28 | ```bash 29 | brew install cnoe-io/tap/idpbuilder@ 30 | ``` 31 | + Nightly Version 32 | 33 | ```bash 34 | brew install cnoe-io/tap/idpbuilder-nightly 35 | ``` 36 | 37 | ### From Releases 38 | Another way to get started is to grab the idpbuilder binary for your platform and run it. You can visit our [releases](https://github.com/cnoe-io/idpbuilder/releases) page to download the version for your system, or run the following commands: 39 | 40 | ```bash 41 | arch=$(if [[ "$(uname -m)" == "x86_64" ]]; then echo "amd64"; else uname -m; fi) 42 | os=$(uname -s | tr '[:upper:]' '[:lower:]') 43 | 44 | 45 | idpbuilder_latest_tag=$(curl --silent "https://api.github.com/repos/cnoe-io/idpbuilder/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') 46 | curl -LO https://github.com/cnoe-io/idpbuilder/releases/download/$idpbuilder_latest_tag/idpbuilder-$os-$arch.tar.gz 47 | tar xvzf idpbuilder-$os-$arch.tar.gz 48 | ``` 49 | 50 | Download latest extract idpbuilder binary 51 | ```bash 52 | cd ~/bin 53 | curl -vskL -O https://github.com/cnoe-io/idpbuilder/releases/latest/download/idpbuilder-linux-amd64.tar.gz 54 | tar xvzf idpbuilder-linux-amd64.tar.gz idpbuilder 55 | ``` 56 | 57 | ## Getting Started 58 | 59 | You can then run idpbuilder with the create argument to spin up your CNOE IDP: 60 | 61 | ```bash 62 | ./idpbuilder create 63 | ``` 64 | 65 | For more detailed information, checkout our [documentation](https://cnoe.io/docs/idpbuilder) on getting started with idpbuilder. 66 | 67 | ## Community 68 | 69 | - If you have questions or concerns about this tool, please feel free to reach out to us on the [CNCF Slack Channel](https://cloud-native.slack.com/archives/C05TN9WFN5S). 70 | - You can also join our community meetings to meet the team and ask any questions. Checkout [this calendar](https://calendar.google.com/calendar/embed?src=064a2adfce866ccb02e61663a09f99147f22f06374e7a8994066bdc81e066986%40group.calendar.google.com&ctz=America%2FLos_Angeles) for more information. 71 | 72 | ## Contribution 73 | 74 | Checkout the [contribution doc](./CONTRIBUTING.md) for contribution guidelines and more information on how to set up your local environment. 75 | 76 | 77 | 78 | [codespell-badge]: https://github.com/cnoe-io/idpbuilder/actions/workflows/codespell.yaml/badge.svg 79 | [codespell-link]: https://github.com/cnoe-io/idpbuilder/actions/workflows/codespell.yaml 80 | 81 | [e2e-badge]: https://github.com/cnoe-io/idpbuilder/actions/workflows/e2e.yaml/badge.svg 82 | [e2e-link]: https://github.com/cnoe-io/idpbuilder/actions/workflows/e2e.yaml 83 | 84 | [report-badge]: https://goreportcard.com/badge/github.com/cnoe-io/idpbuilder 85 | [report-link]: https://goreportcard.com/report/github.com/cnoe-io/idpbuilder 86 | 87 | [commit-activity-badge]: https://img.shields.io/github/commit-activity/m/cnoe-io/idpbuilder 88 | [commit-activity-link]: https://github.com/cnoe-io/idpbuilder/pulse 89 | -------------------------------------------------------------------------------- /pkg/util/git_repository_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/cnoe-io/idpbuilder/api/v1alpha1" 11 | "github.com/go-git/go-billy/v5" 12 | "github.com/go-git/go-billy/v5/memfs" 13 | "github.com/go-git/go-git/v5" 14 | "github.com/go-git/go-git/v5/storage/memory" 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | func TestCloneRemoteRepoToDir(t *testing.T) { 19 | spec := v1alpha1.RemoteRepositorySpec{ 20 | CloneSubmodules: false, 21 | Path: "examples/basic", 22 | Url: "https://github.com/cnoe-io/idpbuilder", 23 | Ref: "v0.3.0", 24 | } 25 | dir, _ := os.MkdirTemp("", "TestCopyToDir") 26 | defer os.RemoveAll(dir) 27 | // new clone 28 | _, _, err := CloneRemoteRepoToDir(context.Background(), spec, 0, false, dir, "") 29 | assert.Nil(t, err) 30 | testDir, _ := os.MkdirTemp("", "TestCopyToDir") 31 | defer os.RemoveAll(testDir) 32 | 33 | repo, err := git.PlainClone(testDir, false, &git.CloneOptions{URL: dir}) 34 | assert.Nil(t, err) 35 | ref, err := repo.Head() 36 | assert.Nil(t, err) 37 | assert.Equal(t, "52783df3a8942cc882ebeb6168f80e1876a2f129", ref.Hash().String()) 38 | 39 | // existing 40 | spec.Ref = "v0.4.0" 41 | testDir2, _ := os.MkdirTemp("", "TestCopyToDir") 42 | defer os.RemoveAll(testDir2) 43 | 44 | _, _, err = CloneRemoteRepoToDir(context.Background(), spec, 0, false, dir, "") 45 | repo, err = git.PlainClone(testDir2, false, &git.CloneOptions{URL: dir}) 46 | assert.Nil(t, err) 47 | ref, err = repo.Head() 48 | assert.Nil(t, err) 49 | assert.Equal(t, "11eccd57fde9f4ef6de8bfa1fc11d168a4d30fe1", ref.Hash().String()) 50 | 51 | assert.Nil(t, err) 52 | } 53 | 54 | func TestCopyTreeToTree(t *testing.T) { 55 | spec := v1alpha1.RemoteRepositorySpec{ 56 | CloneSubmodules: false, 57 | Path: "examples/basic", 58 | Url: "https://github.com/cnoe-io/idpbuilder", 59 | Ref: "", 60 | } 61 | 62 | dst := memfs.New() 63 | src, _, err := CloneRemoteRepoToMemory(context.Background(), spec, 1, false) 64 | assert.Nil(t, err) 65 | 66 | err = CopyTreeToTree(src, dst, spec.Path, ".") 67 | assert.Nil(t, err) 68 | testCopiedFiles(t, src, dst, spec.Path, ".") 69 | } 70 | 71 | func testCopiedFiles(t *testing.T, src, dst billy.Filesystem, srcStartPath, dstStartPath string) { 72 | files, err := src.ReadDir(srcStartPath) 73 | assert.Nil(t, err) 74 | 75 | for i := range files { 76 | file := files[i] 77 | if file.Mode().IsRegular() { 78 | srcB, err := ReadWorktreeFile(src, filepath.Join(srcStartPath, file.Name())) 79 | assert.Nil(t, err) 80 | 81 | dstB, err := ReadWorktreeFile(dst, filepath.Join(dstStartPath, file.Name())) 82 | assert.Nil(t, err) 83 | assert.Equal(t, srcB, dstB) 84 | } 85 | if file.IsDir() { 86 | testCopiedFiles(t, src, dst, filepath.Join(srcStartPath, file.Name()), filepath.Join(dstStartPath, file.Name())) 87 | } 88 | } 89 | } 90 | 91 | func TestGetWorktreeYamlFiles(t *testing.T) { 92 | filepath.Join() 93 | cloneOptions := &git.CloneOptions{ 94 | URL: "https://github.com/cnoe-io/idpbuilder", 95 | Depth: 1, 96 | ShallowSubmodules: true, 97 | } 98 | 99 | wt := memfs.New() 100 | _, err := git.CloneContext(context.Background(), memory.NewStorage(), wt, cloneOptions) 101 | if err != nil { 102 | t.Fatalf(err.Error()) 103 | } 104 | 105 | paths, err := GetWorktreeYamlFiles("./pkg", wt, true) 106 | 107 | assert.Equal(t, nil, err) 108 | assert.NotEqual(t, 0, len(paths)) 109 | for _, s := range paths { 110 | assert.Equal(t, true, strings.HasSuffix(s, "yaml") || strings.HasSuffix(s, "yml")) 111 | } 112 | 113 | paths, err = GetWorktreeYamlFiles("./pkg", wt, false) 114 | assert.Equal(t, nil, err) 115 | assert.Equal(t, 0, len(paths)) 116 | } 117 | -------------------------------------------------------------------------------- /pkg/util/util_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | 7 | "github.com/cnoe-io/idpbuilder/api/v1alpha1" 8 | "github.com/stretchr/testify/assert" 9 | "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/apimachinery/pkg/runtime" 11 | "k8s.io/apimachinery/pkg/runtime/schema" 12 | ) 13 | 14 | var specialCharMap = make(map[string]struct{}) 15 | 16 | func TestGeneratePassword(t *testing.T) { 17 | for i := range specialChars { 18 | specialCharMap[string(specialChars[i])] = struct{}{} 19 | } 20 | 21 | for i := 0; i < 1000; i++ { 22 | p, err := GeneratePassword() 23 | if err != nil { 24 | t.Fatalf("error generating password: %v", err) 25 | } 26 | counts := make([]int, 3) 27 | for j := range p { 28 | counts[0] += 1 29 | c := string(p[j]) 30 | _, ok := specialCharMap[c] 31 | if ok { 32 | counts[1] += 1 33 | continue 34 | } 35 | _, err := strconv.Atoi(c) 36 | if err == nil { 37 | counts[2] += 1 38 | } 39 | } 40 | if counts[0] != passwordLength { 41 | t.Fatalf("password length incorrect") 42 | } 43 | if counts[1] < numSpecialChars { 44 | t.Fatalf("min number of special chars not generated") 45 | } 46 | if counts[2] < numDigits { 47 | t.Fatalf("min number of digits not generated") 48 | } 49 | } 50 | } 51 | 52 | type MockObject struct { 53 | v1.ObjectMeta 54 | } 55 | 56 | func (m *MockObject) GetObjectKind() schema.ObjectKind { 57 | return nil 58 | } 59 | 60 | func (m *MockObject) DeepCopyObject() runtime.Object { 61 | return nil 62 | } 63 | 64 | func TestSetPackageLabels(t *testing.T) { 65 | testCases := []struct { 66 | name string 67 | objectName string 68 | initialLabels map[string]string 69 | expectedLabels map[string]string 70 | }{ 71 | { 72 | name: "No initial labels", 73 | objectName: "test-package", 74 | initialLabels: nil, 75 | expectedLabels: map[string]string{ 76 | v1alpha1.PackageNameLabelKey: "test-package", 77 | v1alpha1.PackageTypeLabelKey: v1alpha1.PackageTypeLabelCustom, 78 | }, 79 | }, 80 | { 81 | name: "With initial labels", 82 | objectName: "test-package-one", 83 | initialLabels: map[string]string{ 84 | "existing": "label", 85 | v1alpha1.PackageNameLabelKey: "incorrect", 86 | }, 87 | expectedLabels: map[string]string{ 88 | "existing": "label", 89 | v1alpha1.PackageNameLabelKey: "test-package-one", 90 | v1alpha1.PackageTypeLabelKey: v1alpha1.PackageTypeLabelCustom, 91 | }, 92 | }, 93 | { 94 | name: "ArgoCD package", 95 | objectName: v1alpha1.ArgoCDPackageName, 96 | initialLabels: nil, 97 | expectedLabels: map[string]string{ 98 | v1alpha1.PackageNameLabelKey: v1alpha1.ArgoCDPackageName, 99 | v1alpha1.PackageTypeLabelKey: v1alpha1.PackageTypeLabelCore, 100 | }, 101 | }, 102 | { 103 | name: "Gitea package", 104 | objectName: v1alpha1.GiteaPackageName, 105 | initialLabels: nil, 106 | expectedLabels: map[string]string{ 107 | v1alpha1.PackageNameLabelKey: v1alpha1.GiteaPackageName, 108 | v1alpha1.PackageTypeLabelKey: v1alpha1.PackageTypeLabelCore, 109 | }, 110 | }, 111 | { 112 | name: "IngressNginx package", 113 | objectName: v1alpha1.IngressNginxPackageName, 114 | initialLabels: nil, 115 | expectedLabels: map[string]string{ 116 | v1alpha1.PackageNameLabelKey: v1alpha1.IngressNginxPackageName, 117 | v1alpha1.PackageTypeLabelKey: v1alpha1.PackageTypeLabelCore, 118 | }, 119 | }, 120 | } 121 | 122 | for _, tc := range testCases { 123 | t.Run(tc.name, func(t *testing.T) { 124 | obj := &MockObject{ 125 | ObjectMeta: v1.ObjectMeta{ 126 | Name: tc.objectName, 127 | Labels: tc.initialLabels, 128 | }, 129 | } 130 | 131 | SetPackageLabels(obj) 132 | 133 | assert.Equal(t, tc.expectedLabels, obj.GetLabels()) 134 | }) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /hack/gitea/ingress.yaml.tmpl: -------------------------------------------------------------------------------- 1 | {{- if .UsePathRouting }} 2 | --- 3 | apiVersion: networking.k8s.io/v1 4 | kind: Ingress 5 | metadata: 6 | name: my-gitea-path-oci-root 7 | namespace: gitea 8 | annotations: 9 | nginx.ingress.kubernetes.io/proxy-body-size: 1024m 10 | spec: 11 | ingressClassName: nginx 12 | rules: 13 | - host: {{ .IngressHost }} 14 | http: 15 | paths: 16 | - backend: 17 | service: 18 | name: my-gitea-http 19 | port: 20 | number: 3000 21 | path: /v2 22 | pathType: Prefix 23 | {{- if ne .IngressHost .Host }} 24 | - host: {{ .Host }} 25 | http: 26 | paths: 27 | - backend: 28 | service: 29 | name: my-gitea-http 30 | port: 31 | number: 3000 32 | path: /v2 33 | pathType: Prefix 34 | {{ end }} 35 | --- 36 | apiVersion: networking.k8s.io/v1 37 | kind: Ingress 38 | metadata: 39 | name: my-gitea-path-oci-repo 40 | namespace: gitea 41 | annotations: 42 | nginx.ingress.kubernetes.io/proxy-body-size: 1024m 43 | nginx.ingress.kubernetes.io/use-regex: "true" 44 | nginx.ingress.kubernetes.io/rewrite-target: /v2/$2 45 | spec: 46 | ingressClassName: nginx 47 | rules: 48 | - host: {{ .IngressHost }} 49 | http: 50 | paths: 51 | - backend: 52 | service: 53 | name: my-gitea-http 54 | port: 55 | number: 3000 56 | path: /v2/gitea(/|$)(.*) 57 | pathType: ImplementationSpecific 58 | {{- if ne .IngressHost .Host }} 59 | - host: {{ .Host }} 60 | http: 61 | paths: 62 | - backend: 63 | service: 64 | name: my-gitea-http 65 | port: 66 | number: 3000 67 | path: /v2/gitea(/|$)(.*) 68 | pathType: ImplementationSpecific 69 | {{ end }} 70 | --- 71 | apiVersion: networking.k8s.io/v1 72 | kind: Ingress 73 | metadata: 74 | name: my-gitea-path 75 | namespace: gitea 76 | annotations: 77 | nginx.ingress.kubernetes.io/proxy-body-size: 1024m 78 | nginx.ingress.kubernetes.io/use-regex: "true" 79 | nginx.ingress.kubernetes.io/rewrite-target: /$2 80 | spec: 81 | ingressClassName: nginx 82 | rules: 83 | - host: {{ .IngressHost }} 84 | http: 85 | paths: 86 | - backend: 87 | service: 88 | name: my-gitea-http 89 | port: 90 | number: 3000 91 | path: /gitea(/|$)(.*) 92 | pathType: ImplementationSpecific 93 | {{- if ne .IngressHost .Host }} 94 | - host: {{ .Host }} 95 | http: 96 | paths: 97 | - backend: 98 | service: 99 | name: my-gitea-http 100 | port: 101 | number: 3000 102 | path: /gitea(/|$)(.*) 103 | pathType: ImplementationSpecific 104 | {{ end }} 105 | {{ else }} 106 | --- 107 | apiVersion: networking.k8s.io/v1 108 | kind: Ingress 109 | metadata: 110 | name: my-gitea-custom 111 | namespace: gitea 112 | annotations: 113 | nginx.ingress.kubernetes.io/proxy-body-size: 1024m 114 | spec: 115 | ingressClassName: nginx 116 | rules: 117 | - host: gitea.{{ .IngressHost }} 118 | http: 119 | paths: 120 | - path: / 121 | pathType: Prefix 122 | backend: 123 | service: 124 | name: my-gitea-http 125 | port: 126 | number: 3000 127 | {{- if ne .IngressHost .Host }} 128 | - host: gitea.{{ .Host }} 129 | http: 130 | paths: 131 | - path: / 132 | pathType: Prefix 133 | backend: 134 | service: 135 | name: my-gitea-http 136 | port: 137 | number: 3000 138 | {{ end }} 139 | {{ end }} 140 | -------------------------------------------------------------------------------- /pkg/controllers/gitrepository/github.go: -------------------------------------------------------------------------------- 1 | package gitrepository 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/cnoe-io/idpbuilder/api/v1alpha1" 9 | "github.com/cnoe-io/idpbuilder/pkg/util" 10 | "github.com/google/go-github/v61/github" 11 | v1 "k8s.io/api/core/v1" 12 | "k8s.io/apimachinery/pkg/runtime" 13 | "k8s.io/apimachinery/pkg/types" 14 | "sigs.k8s.io/controller-runtime/pkg/client" 15 | ) 16 | 17 | const ( 18 | gitHubTokenKey = "token" 19 | ) 20 | 21 | type ghClient struct { 22 | c *github.Client 23 | } 24 | 25 | func (g *ghClient) getRepo(ctx context.Context, owner, repo string) (*github.Repository, *github.Response, error) { 26 | return g.c.Repositories.Get(ctx, owner, repo) 27 | } 28 | 29 | func (g *ghClient) createRepo(ctx context.Context, owner string, req *github.Repository) (*github.Repository, *github.Response, error) { 30 | return g.c.Repositories.Create(ctx, owner, req) 31 | } 32 | 33 | func (g *ghClient) setToken(token string) error { 34 | g.c = g.c.WithAuthToken(token) 35 | return nil 36 | } 37 | 38 | type gitHubProvider struct { 39 | client.Client 40 | Scheme *runtime.Scheme 41 | gitHubClient gitHubClient 42 | config v1alpha1.BuildCustomizationSpec 43 | } 44 | 45 | func (g *gitHubProvider) createRepository(ctx context.Context, repo *v1alpha1.GitRepository) (repoInfo, error) { 46 | req := github.Repository{ 47 | Name: github.String(getRepositoryName(*repo)), 48 | Private: github.Bool(true), 49 | } 50 | r, _, err := g.gitHubClient.createRepo(ctx, getOrganizationName(*repo), &req) 51 | if err != nil { 52 | return repoInfo{}, fmt.Errorf("creating repo: %w", err) 53 | } 54 | 55 | return repoInfo{ 56 | name: *r.Name, 57 | cloneUrl: *r.CloneURL, 58 | internalGitRepositoryUrl: "", 59 | fullName: *r.FullName, 60 | }, nil 61 | } 62 | 63 | func (g *gitHubProvider) getRepository(ctx context.Context, repo *v1alpha1.GitRepository) (repoInfo, error) { 64 | r, resp, err := g.gitHubClient.getRepo(ctx, getOrganizationName(*repo), getRepositoryName(*repo)) 65 | if err != nil { 66 | if resp != nil && resp.StatusCode == http.StatusNotFound { 67 | return repoInfo{}, notFoundError{} 68 | } else { 69 | return repoInfo{}, fmt.Errorf("getting repo: %w", err) 70 | } 71 | } 72 | 73 | return repoInfo{ 74 | name: *r.Name, 75 | cloneUrl: *r.CloneURL, 76 | internalGitRepositoryUrl: "", 77 | fullName: *r.FullName, 78 | }, nil 79 | } 80 | 81 | func (g *gitHubProvider) getProviderCredentials(ctx context.Context, repo *v1alpha1.GitRepository) (gitProviderCredentials, error) { 82 | var secret v1.Secret 83 | err := g.Client.Get(ctx, types.NamespacedName{ 84 | Namespace: repo.Spec.SecretRef.Namespace, 85 | Name: repo.Spec.SecretRef.Name, 86 | }, &secret) 87 | if err != nil { 88 | return gitProviderCredentials{}, err 89 | } 90 | 91 | token, ok := secret.Data[gitHubTokenKey] 92 | if !ok { 93 | return gitProviderCredentials{}, fmt.Errorf("%s key not found in secret %s in %s ns", giteaAdminUsernameKey, repo.Spec.SecretRef.Name, repo.Spec.SecretRef.Namespace) 94 | } 95 | 96 | return gitProviderCredentials{ 97 | accessToken: string(token), 98 | }, nil 99 | } 100 | 101 | func (g *gitHubProvider) setProviderCredentials(ctx context.Context, repo *v1alpha1.GitRepository, creds gitProviderCredentials) error { 102 | return g.gitHubClient.setToken(creds.accessToken) 103 | } 104 | 105 | func (g *gitHubProvider) updateRepoContent( 106 | ctx context.Context, 107 | repo *v1alpha1.GitRepository, 108 | repoInfo repoInfo, 109 | creds gitProviderCredentials, 110 | tmpDir string, 111 | repoMap *util.RepoMap, 112 | ) error { 113 | return reconcileLocalRepoContent(ctx, repo, repoInfo, creds, g.Scheme, g.config, tmpDir, repoMap) 114 | } 115 | 116 | func newGitHubClient(httpClient *http.Client) gitHubClient { 117 | return &ghClient{ 118 | c: github.NewClient(httpClient), 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /pkg/kind/config.go: -------------------------------------------------------------------------------- 1 | package kind 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | "io" 7 | "io/fs" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | 12 | "github.com/cnoe-io/idpbuilder/api/v1alpha1" 13 | "github.com/cnoe-io/idpbuilder/pkg/util/files" 14 | ) 15 | 16 | type PortMapping struct { 17 | HostPort string 18 | ContainerPort string 19 | } 20 | 21 | type TemplateConfig struct { 22 | v1alpha1.BuildCustomizationSpec 23 | KubernetesVersion string 24 | ExtraPortsMapping []PortMapping 25 | RegistryConfig string 26 | RegistryCertsDir string 27 | } 28 | 29 | //go:embed resources/* testdata/custom-kind.yaml.tmpl 30 | var configFS embed.FS 31 | 32 | func loadConfig(path string, httpClient HttpClient) ([]byte, error) { 33 | var rawConfigTempl []byte 34 | var err error 35 | if path != "" { 36 | if strings.HasPrefix(path, "https://") || strings.HasPrefix(path, "http://") { 37 | resp, err := httpClient.Get(path) 38 | if err != nil { 39 | return nil, fmt.Errorf("fetching remote kind config: %w", err) 40 | } 41 | defer resp.Body.Close() 42 | if !(resp.StatusCode < 300 && resp.StatusCode >= 200) { 43 | return nil, fmt.Errorf("got %d status code when fetching kind config", resp.StatusCode) 44 | } 45 | rawConfigTempl, err = io.ReadAll(resp.Body) 46 | if err != nil { 47 | return nil, fmt.Errorf("reading remote kind config body: %w", err) 48 | } 49 | } else { 50 | rawConfigTempl, err = os.ReadFile(path) 51 | } 52 | } else { 53 | rawConfigTempl, err = fs.ReadFile(configFS, "resources/kind.yaml.tmpl") 54 | } 55 | 56 | if err != nil { 57 | return nil, fmt.Errorf("reading kind config: %w", err) 58 | } 59 | return rawConfigTempl, nil 60 | } 61 | 62 | func parsePortMappings(extraPortsMapping string) []PortMapping { 63 | var portMappingPairs []PortMapping 64 | if len(extraPortsMapping) > 0 { 65 | // Split pairs of ports "11=1111","22=2222",etc 66 | pairs := strings.Split(extraPortsMapping, ",") 67 | // Create a slice to store PortMapping pairs. 68 | portMappingPairs = make([]PortMapping, len(pairs)) 69 | // Parse each pair into PortPair objects. 70 | for i, pair := range pairs { 71 | parts := strings.Split(pair, ":") 72 | if len(parts) == 2 { 73 | portMappingPairs[i] = PortMapping{parts[0], parts[1]} 74 | } 75 | } 76 | } 77 | return portMappingPairs 78 | } 79 | 80 | func findRegistryConfig(registryConfigPaths []string) string { 81 | for _, s := range registryConfigPaths { 82 | path := os.ExpandEnv(s) 83 | if _, err := os.Stat(path); err == nil { 84 | return path 85 | } 86 | } 87 | return "" 88 | } 89 | 90 | func renderRegistryCertsDir(cfg v1alpha1.BuildCustomizationSpec) (string, error) { 91 | // Render out the template 92 | rawConfigTempl, err := fs.ReadFile(configFS, "resources/hosts.toml.tmpl") 93 | if err != nil { 94 | return "", fmt.Errorf("reading insecure registry config %w", err) 95 | } 96 | 97 | var retBuff []byte 98 | if retBuff, err = files.ApplyTemplate(rawConfigTempl, cfg); err != nil { 99 | return "", fmt.Errorf("templating insecure registry config %w", err) 100 | } 101 | 102 | // Generate the directory structure and write the file to hosts.toml 103 | dir, err := os.MkdirTemp("", "idpbuilder-registry-certs.d-*") 104 | if err != nil { 105 | return "", fmt.Errorf("creating temp dir %w", err) 106 | } 107 | 108 | var hostAndPort string 109 | if cfg.UsePathRouting { 110 | hostAndPort = fmt.Sprintf("%s:%s", cfg.Host, cfg.Port) 111 | } else { 112 | hostAndPort = fmt.Sprintf("gitea.%s:%s", cfg.Host, cfg.Port) 113 | } 114 | hostCertsDir := filepath.Join(dir, hostAndPort) 115 | err = os.Mkdir(hostCertsDir, 0700) 116 | if err != nil { 117 | return "", fmt.Errorf("creating temp dir for host %w", err) 118 | } 119 | hostsFile := filepath.Join(hostCertsDir, "hosts.toml") 120 | 121 | err = os.WriteFile(hostsFile, retBuff, 0700) 122 | if err != nil { 123 | return "", fmt.Errorf("writing insecure registry config %w", err) 124 | } 125 | 126 | return dir, nil 127 | } 128 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | LD_FLAGS=-ldflags " \ 2 | -X github.com/cnoe-io/idpbuilder/pkg/cmd/version.idpbuilderVersion=$(shell git describe --always --tags --dirty --broken) \ 3 | -X github.com/cnoe-io/idpbuilder/pkg/cmd/version.gitCommit=$(shell git rev-parse HEAD) \ 4 | -X github.com/cnoe-io/idpbuilder/pkg/cmd/version.buildDate=$(shell date -u +'%Y-%m-%dT%H:%M:%SZ') \ 5 | " 6 | 7 | # The name of the binary. Defaults to idpbuilder 8 | OUT_FILE ?= idpbuilder 9 | 10 | .PHONY: build 11 | build: manifests generate fmt vet embedded-resources 12 | go build $(LD_FLAGS) -o $(OUT_FILE) main.go 13 | 14 | # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. 15 | ENVTEST_K8S_VERSION = 1.29.1 16 | 17 | ## Location to install dependencies to 18 | LOCALBIN ?= $(shell pwd)/bin 19 | $(LOCALBIN): 20 | mkdir -p $(LOCALBIN) 21 | CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen 22 | ENVTEST ?= $(LOCALBIN)/setup-envtest 23 | KUSTOMIZE ?= $(LOCALBIN)/kustomize 24 | HELM_TGZ ?= $(LOCALBIN)/helm.tar.gz 25 | HELM ?= $(LOCALBIN)/helm 26 | 27 | ## Tool Versions 28 | CONTROLLER_TOOLS_VERSION ?= v0.15.0 29 | 30 | .PHONY: fmt 31 | fmt: ## Run go fmt against code. 32 | go fmt ./... 33 | 34 | .PHONY: vet 35 | vet: ## Run go vet against code. 36 | go vet ./... 37 | 38 | .PHONY: test 39 | test: manifests generate fmt vet envtest ## Run tests. 40 | ifeq ($(RUN),) 41 | KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test -p 1 --tags=integration ./... -coverprofile cover.out 42 | else 43 | KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test -p 1 --tags=integration ./... -coverprofile cover.out -run $(RUN) 44 | endif 45 | 46 | 47 | 48 | .PHONY: generate 49 | generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. 50 | $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./api/..." 51 | 52 | .PHONY: manifests 53 | manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. 54 | $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./api/..." output:crd:artifacts:config=pkg/controllers/resources 55 | 56 | .PHONY: controller-gen 57 | controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. If wrong version is installed, it will be overwritten. 58 | $(CONTROLLER_GEN): $(LOCALBIN) 59 | test -s $(LOCALBIN)/controller-gen && $(LOCALBIN)/controller-gen --version | grep -q $(CONTROLLER_TOOLS_VERSION) || \ 60 | GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION) 61 | 62 | .PHONY: kustomize 63 | kustomize: ## Download kustomize if necessary 64 | ifeq (,$(wildcard $(KUSTOMIZE))) 65 | cd $(LOCALBIN) && curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash 66 | endif 67 | 68 | helm_os := $(shell uname | tr '[:upper:]' '[:lower:]') 69 | helm_version ?= 3.15.0 70 | ifeq ($(shell uname -m), x86_64) 71 | helm_arch ?= amd64 72 | endif 73 | ifeq ($(shell uname -m), arm64) 74 | helm_arch ?= arm64 75 | endif 76 | ifeq ($(shell uname -m), aarch64) 77 | helm_arch ?= arm64 78 | endif 79 | 80 | 81 | .PHONY: helm 82 | helm: ## Download helm if necessary 83 | ifeq (,$(wildcard $(HELM))) 84 | curl https://get.helm.sh/helm-v$(helm_version)-$(helm_os)-$(helm_arch).tar.gz -o $(HELM_TGZ) 85 | tar xvzf $(HELM_TGZ) -C $(LOCALBIN) --strip-components 1 $(helm_os)-$(helm_arch)/helm 86 | chmod +x $(HELM) 87 | endif 88 | 89 | .PHONY: envtest 90 | envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. 91 | $(ENVTEST): $(LOCALBIN) 92 | test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest 93 | 94 | .PHONY: embedded-resources 95 | embedded-resources: kustomize helm 96 | export PATH=$(LOCALBIN):$$PATH; ./hack/embedded-resources.sh; 97 | 98 | .PHONY: e2e 99 | e2e: build 100 | go test -v -p 1 -timeout 15m --tags=e2e ./tests/e2e/... 101 | -------------------------------------------------------------------------------- /pkg/build/templates/coredns/deployment-coredns.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | k8s-app: kube-dns 6 | name: coredns 7 | namespace: kube-system 8 | spec: 9 | progressDeadlineSeconds: 600 10 | replicas: 2 11 | revisionHistoryLimit: 10 12 | selector: 13 | matchLabels: 14 | k8s-app: kube-dns 15 | strategy: 16 | rollingUpdate: 17 | maxSurge: 25% 18 | maxUnavailable: 1 19 | type: RollingUpdate 20 | template: 21 | metadata: 22 | creationTimestamp: null 23 | labels: 24 | k8s-app: kube-dns 25 | spec: 26 | affinity: 27 | podAntiAffinity: 28 | preferredDuringSchedulingIgnoredDuringExecution: 29 | - podAffinityTerm: 30 | labelSelector: 31 | matchExpressions: 32 | - key: k8s-app 33 | operator: In 34 | values: 35 | - kube-dns 36 | topologyKey: kubernetes.io/hostname 37 | weight: 100 38 | containers: 39 | - args: 40 | - -conf 41 | - /etc/coredns/Corefile 42 | image: registry.k8s.io/coredns/coredns:v1.11.1 43 | imagePullPolicy: IfNotPresent 44 | livenessProbe: 45 | failureThreshold: 5 46 | httpGet: 47 | path: /health 48 | port: 8080 49 | scheme: HTTP 50 | initialDelaySeconds: 60 51 | periodSeconds: 10 52 | successThreshold: 1 53 | timeoutSeconds: 5 54 | name: coredns 55 | ports: 56 | - containerPort: 53 57 | name: dns 58 | protocol: UDP 59 | - containerPort: 53 60 | name: dns-tcp 61 | protocol: TCP 62 | - containerPort: 9153 63 | name: metrics 64 | protocol: TCP 65 | readinessProbe: 66 | failureThreshold: 3 67 | httpGet: 68 | path: /ready 69 | port: 8181 70 | scheme: HTTP 71 | periodSeconds: 10 72 | successThreshold: 1 73 | timeoutSeconds: 1 74 | resources: 75 | limits: 76 | memory: 170Mi 77 | requests: 78 | cpu: 100m 79 | memory: 70Mi 80 | securityContext: 81 | allowPrivilegeEscalation: false 82 | capabilities: 83 | add: 84 | - NET_BIND_SERVICE 85 | drop: 86 | - ALL 87 | readOnlyRootFilesystem: true 88 | terminationMessagePath: /dev/termination-log 89 | terminationMessagePolicy: File 90 | volumeMounts: 91 | - mountPath: /etc/coredns 92 | name: config-volume 93 | readOnly: true 94 | - mountPath: /etc/coredns-configs 95 | name: custom-configs 96 | readOnly: true 97 | dnsPolicy: Default 98 | nodeSelector: 99 | kubernetes.io/os: linux 100 | priorityClassName: system-cluster-critical 101 | restartPolicy: Always 102 | schedulerName: default-scheduler 103 | securityContext: {} 104 | serviceAccount: coredns 105 | serviceAccountName: coredns 106 | terminationGracePeriodSeconds: 30 107 | tolerations: 108 | - key: CriticalAddonsOnly 109 | operator: Exists 110 | - effect: NoSchedule 111 | key: node-role.kubernetes.io/control-plane 112 | volumes: 113 | - configMap: 114 | defaultMode: 420 115 | items: 116 | - key: Corefile 117 | path: Corefile 118 | name: coredns 119 | name: config-volume 120 | - name: custom-configs 121 | projected: 122 | sources: 123 | - configMap: 124 | name: coredns-conf-custom 125 | - configMap: 126 | name: coredns-conf-default 127 | -------------------------------------------------------------------------------- /pkg/util/gitea.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "code.gitea.io/sdk/gitea" 5 | "context" 6 | "encoding/base64" 7 | "fmt" 8 | "github.com/cnoe-io/idpbuilder/api/v1alpha1" 9 | corev1 "k8s.io/api/core/v1" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 12 | "sigs.k8s.io/controller-runtime/pkg/client" 13 | "strings" 14 | ) 15 | 16 | const ( 17 | // hardcoded values from what we have in the yaml installation file. 18 | GiteaNamespace = "gitea" 19 | GiteaAdminSecret = "gitea-credential" 20 | GiteaAdminName = "giteaAdmin" 21 | GiteaAdminTokenName = "admin" 22 | GiteaAdminTokenFieldName = "token" 23 | GiteaURLTempl = "%s://%s%s:%s%s" 24 | ) 25 | 26 | func GiteaAdminSecretObject() corev1.Secret { 27 | return corev1.Secret{ 28 | TypeMeta: metav1.TypeMeta{ 29 | Kind: "Secret", 30 | APIVersion: "v1", 31 | }, 32 | ObjectMeta: metav1.ObjectMeta{ 33 | Name: GiteaAdminSecret, 34 | Namespace: GiteaNamespace, 35 | }, 36 | } 37 | } 38 | 39 | func PatchPasswordSecret(ctx context.Context, kubeClient client.Client, config v1alpha1.BuildCustomizationSpec, ns string, secretName string, username string, pass string) error { 40 | sec, err := GetSecretByName(ctx, kubeClient, ns, secretName) 41 | if err != nil { 42 | return fmt.Errorf("getting secret to patch fails: %w", err) 43 | } 44 | u := unstructured.Unstructured{} 45 | u.SetName(sec.GetName()) 46 | u.SetNamespace(sec.GetNamespace()) 47 | u.SetGroupVersionKind(sec.GetObjectKind().GroupVersionKind()) 48 | 49 | err = unstructured.SetNestedField(u.Object, base64.StdEncoding.EncodeToString([]byte(pass)), "data", "password") 50 | if err != nil { 51 | return fmt.Errorf("setting password field: %w", err) 52 | } 53 | 54 | if strings.Contains(secretName, "gitea") { 55 | // We should recreate a token as user/password changed 56 | giteaUrl := GiteaBaseUrl(config) 57 | 58 | t, err := GetGiteaToken(ctx, giteaUrl, string(username), string(pass)) 59 | if err != nil { 60 | return fmt.Errorf("getting gitea token: %w", err) 61 | } 62 | 63 | token := base64.StdEncoding.EncodeToString([]byte(t)) 64 | err = unstructured.SetNestedField(u.Object, token, "data", GiteaAdminTokenFieldName) 65 | if err != nil { 66 | return fmt.Errorf("setting gitea token field: %w", err) 67 | } 68 | } 69 | 70 | return kubeClient.Patch(ctx, &u, client.Apply, client.ForceOwnership, client.FieldOwner(v1alpha1.FieldManager)) 71 | } 72 | 73 | func GetGiteaToken(ctx context.Context, baseUrl, username, password string) (string, error) { 74 | giteaClient, err := gitea.NewClient(baseUrl, gitea.SetHTTPClient(GetHttpClient()), 75 | gitea.SetBasicAuth(username, password), gitea.SetContext(ctx), 76 | ) 77 | if err != nil { 78 | return "", fmt.Errorf("creating gitea client: %w", err) 79 | } 80 | tokens, resp, err := giteaClient.ListAccessTokens(gitea.ListAccessTokensOptions{}) 81 | if err != nil { 82 | return "", fmt.Errorf("listing gitea access tokens. status: %s error : %w", resp.Status, err) 83 | } 84 | 85 | for i := range tokens { 86 | if tokens[i].Name == GiteaAdminTokenName { 87 | resp, err := giteaClient.DeleteAccessToken(tokens[i].ID) 88 | if err != nil { 89 | return "", fmt.Errorf("deleting gitea access tokens. status: %s error : %w", resp.Status, err) 90 | } 91 | break 92 | } 93 | } 94 | 95 | token, resp, err := giteaClient.CreateAccessToken(gitea.CreateAccessTokenOption{ 96 | Name: GiteaAdminTokenName, 97 | Scopes: []gitea.AccessTokenScope{ 98 | gitea.AccessTokenScopeAll, 99 | }, 100 | }) 101 | if err != nil { 102 | return "", fmt.Errorf("deleting gitea access tokens. status: %s error : %w", resp.Status, err) 103 | } 104 | 105 | return token.Token, nil 106 | } 107 | 108 | func GiteaBaseUrl(config v1alpha1.BuildCustomizationSpec) string { 109 | if config.UsePathRouting { 110 | return fmt.Sprintf(GiteaURLTempl, config.Protocol, "", config.Host, config.Port, "/gitea") 111 | } 112 | return fmt.Sprintf(GiteaURLTempl, config.Protocol, "gitea.", config.Host, config.Port, "") 113 | } 114 | -------------------------------------------------------------------------------- /pkg/k8s/deserialize.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "k8s.io/apimachinery/pkg/runtime" 8 | "k8s.io/apimachinery/pkg/runtime/serializer" 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | "sigs.k8s.io/kustomize/kyaml/kio" 11 | kyaml "sigs.k8s.io/kustomize/kyaml/yaml" 12 | ) 13 | 14 | type ConversionError struct { 15 | rtObject runtime.Object 16 | } 17 | 18 | func (e *ConversionError) Error() string { 19 | return fmt.Sprintf("Failed to convert object %q", e.rtObject.GetObjectKind().GroupVersionKind().String()) 20 | } 21 | 22 | func ConvertYamlToObjects(scheme *runtime.Scheme, objYamls []byte) ([]client.Object, error) { 23 | decode := serializer.NewCodecFactory(scheme).UniversalDeserializer().Decode 24 | 25 | var k8sObjects []client.Object 26 | 27 | for _, objYaml := range bytes.Split(objYamls, []byte{'\n', '-', '-', '-', '\n'}) { 28 | if len(objYaml) == 0 { 29 | continue 30 | } 31 | 32 | rtObject, _, err := decode(objYaml, nil, nil) 33 | if err != nil { 34 | return nil, err 35 | } 36 | object, ok := rtObject.(client.Object) 37 | if !ok { 38 | return nil, &ConversionError{rtObject: rtObject} 39 | } 40 | k8sObjects = append(k8sObjects, object) 41 | } 42 | return k8sObjects, nil 43 | } 44 | 45 | func ConvertRawResourcesToObjects(scheme *runtime.Scheme, rawResources [][]byte) ([]client.Object, error) { 46 | var ret []client.Object 47 | for _, resources := range rawResources { 48 | objs, err := ConvertYamlToObjects(scheme, resources) 49 | if err != nil { 50 | return nil, err 51 | } 52 | ret = append(ret, objs...) 53 | } 54 | return ret, nil 55 | } 56 | 57 | // replace k8s objects in given YAML doc with override objects. returns built yaml file and objects 58 | func ConvertYamlToObjectsWithOverride(scheme *runtime.Scheme, originalFiles [][]byte, overrideYamls []byte) ([][]byte, []client.Object, error) { 59 | 60 | overrides, err := kio.FromBytes(overrideYamls) 61 | if err != nil { 62 | return nil, nil, err 63 | } 64 | 65 | overrideMap := make(map[string]*kyaml.RNode) 66 | order := make([]string, 0, len(overrides)) 67 | for i := range overrides { 68 | o := overrides[i] 69 | id := GetObjectIdentifier(o) 70 | overrideMap[id] = o 71 | order = append(order, id) 72 | } 73 | 74 | outYaml := make([][]byte, len(originalFiles)) 75 | outObjs := make([]client.Object, 0, 10) 76 | 77 | for i := range originalFiles { 78 | originalFile := originalFiles[i] 79 | originals, oErr := kio.FromBytes(originalFile) 80 | if oErr != nil { 81 | return nil, nil, oErr 82 | } 83 | 84 | for j := range originals { 85 | id := GetObjectIdentifier(originals[j]) 86 | 87 | o, ok := overrideMap[id] 88 | if ok { 89 | // found an object that needs to be overridden. update manifest and remove from our map. 90 | originals[j].SetYNode(o.YNode()) 91 | delete(overrideMap, id) 92 | } 93 | } 94 | 95 | manifest, oErr := kio.StringAll(originals) 96 | if oErr != nil { 97 | return nil, nil, fmt.Errorf("converting overridden manifest to string: %w", oErr) 98 | } 99 | 100 | objs, oErr := ConvertYamlToObjects(scheme, []byte(manifest)) 101 | if oErr != nil { 102 | return nil, nil, fmt.Errorf("converting overridden manifest to k8s objects: %w", oErr) 103 | } 104 | outObjs = append(outObjs, objs...) 105 | outYaml[i] = []byte(manifest) 106 | } 107 | 108 | // if there are objects that are not overriding any original object, create a new file and add them to it. 109 | if len(overrideMap) != 0 { 110 | // must preserve original order 111 | n := make([]*kyaml.RNode, 0, len(overrideYamls)) 112 | for i := range order { 113 | o, ok := overrideMap[order[i]] 114 | if ok { 115 | n = append(n, o) 116 | } 117 | } 118 | 119 | manifest, err := kio.StringAll(n) 120 | if err != nil { 121 | return nil, nil, fmt.Errorf("converting overridden manifest to string: %w", err) 122 | } 123 | 124 | objs, oErr := ConvertYamlToObjects(scheme, []byte(manifest)) 125 | if oErr != nil { 126 | return nil, nil, fmt.Errorf("converting overridden manifest to k8s objects: %w", oErr) 127 | } 128 | 129 | outObjs = append(outObjs, objs...) 130 | outYaml = append(outYaml, []byte(manifest)) 131 | } 132 | 133 | return outYaml, outObjs, nil 134 | } 135 | 136 | func GetObjectIdentifier(n *kyaml.RNode) string { 137 | return fmt.Sprintf("%s%s%s%s", n.GetApiVersion(), n.GetKind(), n.GetNamespace(), n.GetName()) 138 | } 139 | -------------------------------------------------------------------------------- /pkg/cmd/get/packages.go: -------------------------------------------------------------------------------- 1 | package get 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/cnoe-io/idpbuilder/api/v1alpha1" 7 | "github.com/cnoe-io/idpbuilder/pkg/printer" 8 | "github.com/cnoe-io/idpbuilder/pkg/printer/types" 9 | "github.com/cnoe-io/idpbuilder/pkg/util" 10 | "github.com/spf13/cobra" 11 | "io" 12 | "os" 13 | "sigs.k8s.io/controller-runtime/pkg/client" 14 | "strconv" 15 | ) 16 | 17 | var PackagesCmd = &cobra.Command{ 18 | Use: "packages", 19 | Short: "retrieve packages from the cluster", 20 | Long: ``, 21 | RunE: getPackagesE, 22 | SilenceUsage: true, 23 | } 24 | 25 | func getPackagesE(cmd *cobra.Command, args []string) error { 26 | ctx, ctxCancel := context.WithCancel(cmd.Context()) 27 | defer ctxCancel() 28 | 29 | kubeConfig, err := util.GetKubeConfig() 30 | if err != nil { 31 | return fmt.Errorf("getting kube config: %w", err) 32 | } 33 | 34 | kubeClient, err := util.GetKubeClient(kubeConfig) 35 | if err != nil { 36 | return fmt.Errorf("getting kube client: %w", err) 37 | } 38 | 39 | return printPackages(ctx, os.Stdout, kubeClient, outputFormat) 40 | } 41 | 42 | // Print all the custom packages or based on package arguments passed using flag: -p 43 | func printPackages(ctx context.Context, outWriter io.Writer, kubeClient client.Client, format string) error { 44 | packageList := []types.Package{} 45 | customPackages := v1alpha1.CustomPackageList{} 46 | var err error 47 | 48 | idpbuilderNamespace, err := getIDPNamespace(ctx, kubeClient) 49 | if err != nil { 50 | return fmt.Errorf("getting namespace: %w", err) 51 | } 52 | 53 | config, err := util.GetConfig(ctx) 54 | if err != nil { 55 | return fmt.Errorf("getting idp config: %w", err) 56 | } 57 | 58 | argocdBaseUrl := util.ArgocdBaseUrl(config) 59 | 60 | if len(packages) == 0 { 61 | // Get all custom packages 62 | customPackages, err = getPackages(ctx, kubeClient, idpbuilderNamespace) 63 | if err != nil { 64 | return fmt.Errorf("listing custom packages: %w", err) 65 | } 66 | } else { 67 | // Get the custom package using its name 68 | for _, name := range packages { 69 | cp, err := getPackageByName(ctx, kubeClient, idpbuilderNamespace, name) 70 | if err != nil { 71 | return fmt.Errorf("getting custom package %s: %w", name, err) 72 | } 73 | customPackages.Items = append(customPackages.Items, cp) 74 | } 75 | } 76 | 77 | for _, cp := range customPackages.Items { 78 | newPackage := types.Package{} 79 | newPackage.Name = cp.Name 80 | newPackage.Namespace = cp.Namespace 81 | newPackage.ArgocdRepository = argocdBaseUrl + "/applications/" + cp.Spec.ArgoCD.Namespace + "/" + cp.Spec.ArgoCD.Name 82 | // There is a GitRepositoryRefs when the project has been cloned to the internal git repository 83 | if cp.Status.GitRepositoryRefs != nil { 84 | newPackage.GitRepository = cp.Spec.InternalGitServeURL + "/" + v1alpha1.GiteaAdminUserName + "/" + idpbuilderNamespace + "-" + cp.Status.GitRepositoryRefs[0].Name 85 | } else { 86 | // Default branch reference 87 | ref := "main" 88 | if cp.Spec.RemoteRepository.Ref != "" { 89 | ref = cp.Spec.RemoteRepository.Ref 90 | } 91 | newPackage.GitRepository = cp.Spec.RemoteRepository.Url + "/tree/" + ref + "/" + cp.Spec.RemoteRepository.Path 92 | } 93 | 94 | newPackage.Status = strconv.FormatBool(cp.Status.Synced) 95 | 96 | packageList = append(packageList, newPackage) 97 | } 98 | 99 | packagePrinter := printer.PackagePrinter{ 100 | Packages: packageList, 101 | OutWriter: outWriter, 102 | } 103 | return packagePrinter.PrintOutput(format) 104 | } 105 | 106 | func getPackageByName(ctx context.Context, kubeClient client.Client, ns, name string) (v1alpha1.CustomPackage, error) { 107 | p := v1alpha1.CustomPackage{} 108 | return p, kubeClient.Get(ctx, client.ObjectKey{Name: name, Namespace: ns}, &p) 109 | } 110 | 111 | func getIDPNamespace(ctx context.Context, kubeClient client.Client) (string, error) { 112 | build, err := getLocalBuild(ctx, kubeClient) 113 | if err != nil { 114 | return "", err 115 | } 116 | // TODO: We assume that only one LocalBuild has been created for one cluster ! 117 | idpNamespace := v1alpha1.FieldManager + "-" + build.Items[0].Name 118 | return idpNamespace, nil 119 | } 120 | 121 | func getLocalBuild(ctx context.Context, kubeClient client.Client) (v1alpha1.LocalbuildList, error) { 122 | localBuildList := v1alpha1.LocalbuildList{} 123 | return localBuildList, kubeClient.List(ctx, &localBuildList) 124 | } 125 | 126 | func getPackages(ctx context.Context, kubeClient client.Client, ns string) (v1alpha1.CustomPackageList, error) { 127 | packageList := v1alpha1.CustomPackageList{} 128 | return packageList, kubeClient.List(ctx, &packageList, client.InNamespace(ns)) 129 | } 130 | -------------------------------------------------------------------------------- /pkg/kind/config_test.go: -------------------------------------------------------------------------------- 1 | package kind 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "io/fs" 7 | "net/http" 8 | "reflect" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | type MockHttpClient struct{} 14 | 15 | func (o *MockHttpClient) Get(url string) (resp *http.Response, err error) { 16 | if url == "https://doesnotexist" || url == "http://doesnotexist" { 17 | return nil, errors.New("connection error") 18 | } else if url == "https://404" { 19 | body := io.NopCloser(strings.NewReader("")) 20 | r := http.Response{ 21 | Status: "404 NotFound", 22 | StatusCode: 404, 23 | Body: body, 24 | } 25 | return &r, nil 26 | } 27 | 28 | body := io.NopCloser(strings.NewReader("foo: bar")) 29 | r := http.Response{ 30 | Status: "200 OK", 31 | StatusCode: 200, 32 | Body: body, 33 | } 34 | 35 | return &r, nil 36 | } 37 | 38 | func TestLoadConfig(t *testing.T) { 39 | httpClient := MockHttpClient{} 40 | defaultTemplate, err := fs.ReadFile(configFS, "resources/kind.yaml.tmpl") 41 | if err != nil { 42 | t.Fatalf("failed to load default kind template: %v", err) 43 | } 44 | 45 | customTemplate, err := fs.ReadFile(configFS, "testdata/custom-kind.yaml.tmpl") 46 | if err != nil { 47 | t.Fatalf("failed to load custom kind template: %v", err) 48 | } 49 | 50 | httpsTemplate := []byte("foo: bar") 51 | 52 | connectionErr := "fetching remote kind config: connection error" 53 | notFoundErr := "got 404 status code when fetching kind config" 54 | 55 | type test struct { 56 | path string 57 | expected []byte 58 | err *string 59 | } 60 | tests := []test{ 61 | { 62 | path: "", 63 | expected: defaultTemplate, 64 | err: nil, 65 | }, 66 | { 67 | path: "testdata/custom-kind.yaml.tmpl", 68 | expected: customTemplate, 69 | err: nil, 70 | }, 71 | { 72 | path: "https://doesnotexist", 73 | expected: defaultTemplate, 74 | err: &connectionErr, 75 | }, 76 | { 77 | path: "http://doesnotexist", 78 | expected: customTemplate, 79 | err: &connectionErr, 80 | }, 81 | { 82 | path: "https://404", 83 | expected: defaultTemplate, 84 | err: ¬FoundErr, 85 | }, 86 | { 87 | path: "https://anyurlworks", 88 | expected: httpsTemplate, 89 | err: nil, 90 | }, 91 | } 92 | 93 | for _, tc := range tests { 94 | out, err := loadConfig(tc.path, &httpClient) 95 | if tc.err != nil { 96 | if err != nil { 97 | if err.Error() != *tc.err { 98 | t.Errorf("expected error: %v\nfound error: %v", *tc.err, err.Error()) 99 | } 100 | } else { 101 | t.Errorf("expected error: %v\ndidnt find an error", *tc.err) 102 | } 103 | } else { 104 | if err != nil { 105 | t.Errorf("failed to load kind config: %v", err) 106 | } 107 | if !reflect.DeepEqual(tc.expected, out) { 108 | t.Errorf("expected:\n%v\ngot:\n%v", string(tc.expected), string(out)) 109 | } 110 | } 111 | } 112 | } 113 | 114 | func TestExtraPortMappingsUtilFunc(t *testing.T) { 115 | type test struct { 116 | extraPortMappings string 117 | expected []PortMapping 118 | } 119 | tests := []test{ 120 | { 121 | extraPortMappings: "", 122 | expected: []PortMapping(nil), 123 | }, 124 | { 125 | extraPortMappings: "22:32222", 126 | expected: []PortMapping{ 127 | { 128 | HostPort: "22", 129 | ContainerPort: "32222", 130 | }, 131 | }, 132 | }, 133 | { 134 | extraPortMappings: "11:1111,33:3333,4444:4444", 135 | expected: []PortMapping{ 136 | { 137 | HostPort: "11", 138 | ContainerPort: "1111", 139 | }, 140 | { 141 | HostPort: "33", 142 | ContainerPort: "3333", 143 | }, 144 | { 145 | HostPort: "4444", 146 | ContainerPort: "4444", 147 | }, 148 | }, 149 | }, 150 | } 151 | 152 | for _, tc := range tests { 153 | pmOutput := parsePortMappings(tc.extraPortMappings) 154 | if !reflect.DeepEqual(tc.expected, pmOutput) { 155 | t.Errorf("expected: %v, got: %v", tc.expected, pmOutput) 156 | } 157 | } 158 | } 159 | 160 | func TestFindRegistryConfig(t *testing.T) { 161 | type test struct { 162 | paths []string 163 | expected string 164 | } 165 | tests := []test{ 166 | { 167 | paths: []string{"testdata/empty.json"}, 168 | expected: "testdata/empty.json", 169 | }, 170 | { 171 | paths: []string{"doesntexist"}, 172 | expected: "", 173 | }, 174 | { 175 | paths: []string{"doesntexist", "testdata/empty.json"}, 176 | expected: "testdata/empty.json", 177 | }, 178 | } 179 | 180 | for _, tc := range tests { 181 | out := findRegistryConfig(tc.paths) 182 | if !reflect.DeepEqual(tc.expected, out) { 183 | t.Errorf("expected:\n%v\ngot:\n%v", tc.expected, out) 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /pkg/controllers/localbuild/installer.go: -------------------------------------------------------------------------------- 1 | package localbuild 2 | 3 | import ( 4 | "context" 5 | "embed" 6 | "errors" 7 | "fmt" 8 | "sync" 9 | "time" 10 | 11 | "github.com/cnoe-io/idpbuilder/api/v1alpha1" 12 | "github.com/cnoe-io/idpbuilder/pkg/k8s" 13 | appsv1 "k8s.io/api/apps/v1" 14 | corev1 "k8s.io/api/core/v1" 15 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 16 | "k8s.io/apimachinery/pkg/runtime" 17 | "k8s.io/apimachinery/pkg/runtime/schema" 18 | "k8s.io/apimachinery/pkg/types" 19 | ctrl "sigs.k8s.io/controller-runtime" 20 | "sigs.k8s.io/controller-runtime/pkg/client" 21 | "sigs.k8s.io/controller-runtime/pkg/log" 22 | ) 23 | 24 | var timeout = time.After(5 * time.Minute) 25 | 26 | type EmbeddedInstallation struct { 27 | name string 28 | resourcePath string 29 | namespace string 30 | 31 | // skips waiting on expected resources to become ready 32 | skipReadinessCheck bool 33 | 34 | // name and gvk pair for resources that need to be monitored 35 | monitoredResources map[string]schema.GroupVersionKind 36 | customization v1alpha1.PackageCustomization 37 | resourceFS embed.FS 38 | 39 | // resources that need to be created without using static manifests or gitops 40 | unmanagedResources []client.Object 41 | } 42 | 43 | func (e *EmbeddedInstallation) installResources(scheme *runtime.Scheme, templateData any) ([]client.Object, error) { 44 | return k8s.BuildCustomizedObjects(e.customization.FilePath, e.resourcePath, e.resourceFS, scheme, templateData) 45 | } 46 | 47 | func (e *EmbeddedInstallation) newNamespace(namespace string) *corev1.Namespace { 48 | return &corev1.Namespace{ 49 | ObjectMeta: metav1.ObjectMeta{ 50 | Name: namespace, 51 | }, 52 | } 53 | } 54 | 55 | func (e *EmbeddedInstallation) Install(ctx context.Context, resource *v1alpha1.Localbuild, cli client.Client, sc *runtime.Scheme, cfg v1alpha1.BuildCustomizationSpec) (ctrl.Result, error) { 56 | logger := log.FromContext(ctx) 57 | 58 | nsClient := client.NewNamespacedClient(cli, e.namespace) 59 | installObjs, err := e.installResources(sc, cfg) 60 | if err != nil { 61 | return ctrl.Result{}, err 62 | } 63 | 64 | if err = k8s.EnsureNamespace(ctx, nsClient, e.namespace); err != nil { 65 | return ctrl.Result{}, err 66 | } 67 | 68 | for i := range e.unmanagedResources { 69 | err = k8s.EnsureObject(ctx, nsClient, e.unmanagedResources[i], e.namespace) 70 | if err != nil { 71 | return ctrl.Result{}, err 72 | } 73 | } 74 | 75 | sch := runtime.NewScheme() 76 | appsv1.AddToScheme(sch) 77 | 78 | for _, obj := range installObjs { 79 | // Create object 80 | if err = k8s.EnsureObject(ctx, nsClient, obj, e.namespace); err != nil { 81 | return ctrl.Result{}, err 82 | } 83 | } 84 | 85 | // return early if readiness check is disabled 86 | if e.skipReadinessCheck { 87 | return ctrl.Result{}, nil 88 | } 89 | 90 | // wait for expected resources to become available 91 | errCh := make(chan error) 92 | var wg sync.WaitGroup 93 | 94 | for _, obj := range installObjs { 95 | if gvk, ok := e.monitoredResources[obj.GetName()]; ok { 96 | if obj.GetObjectKind().GroupVersionKind() != gvk { 97 | continue 98 | } 99 | 100 | wg.Add(1) 101 | go func(obj client.Object, gvk schema.GroupVersionKind) { 102 | defer wg.Done() 103 | 104 | gvkObj, err := sch.New(gvk) 105 | if err != nil { 106 | errCh <- err 107 | return 108 | } 109 | 110 | for { 111 | if gotObj, ok := gvkObj.(client.Object); ok { 112 | if err := cli.Get(ctx, types.NamespacedName{Namespace: e.namespace, Name: obj.GetName()}, gotObj); err != nil { 113 | errCh <- err 114 | return 115 | } 116 | 117 | switch t := gotObj.(type) { 118 | case *appsv1.Deployment: 119 | if t.Status.AvailableReplicas >= 1 { 120 | logger.V(1).Info(t.GetName(), "deployment", t.Status.AvailableReplicas) 121 | return 122 | } 123 | case *appsv1.StatefulSet: 124 | if t.Status.AvailableReplicas >= 1 { 125 | logger.V(1).Info(t.GetName(), "statefulset", t.Status.AvailableReplicas) 126 | return 127 | } 128 | } 129 | } 130 | 131 | logger.Info(fmt.Sprintf("Waiting for %s %s to become ready", gvk.Kind, obj.GetName())) 132 | time.Sleep(30 * time.Second) 133 | } 134 | }(obj, gvk) 135 | } 136 | } 137 | 138 | go func() { 139 | wg.Wait() 140 | close(errCh) 141 | }() 142 | 143 | select { 144 | case <-timeout: 145 | err := errors.New("Timeout") 146 | logger.Error(err, fmt.Sprintf("Didn't reconcile %s on time", e.name)) 147 | return ctrl.Result{}, err 148 | case err, errOccurred := <-errCh: 149 | if !errOccurred { 150 | logger.V(1).Info(fmt.Sprintf("%s is ready!", e.name)) 151 | } else { 152 | logger.Error(err, fmt.Sprintf("failed to reconcile the %s resources", e.name)) 153 | return ctrl.Result{}, err 154 | } 155 | } 156 | 157 | return ctrl.Result{}, nil 158 | } 159 | -------------------------------------------------------------------------------- /pkg/util/url.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "strconv" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // constants from remote target parameters supported by Kustomize 12 | // https://github.com/kubernetes-sigs/kustomize/blob/master/examples/remoteBuild.md 13 | const ( 14 | QueryStringRef = "ref" 15 | QueryStringVersion = "version" 16 | QueryStringTimeout = "timeout" 17 | QueryStringSubmodules = "submodules" 18 | 19 | RepoUrlDelimiter = "//" 20 | SCPDelimiter = ":" 21 | UserDelimiter = "@" 22 | 23 | defaultTimeout = time.Second * 27 24 | defaultCloneSubmodule = true 25 | 26 | errMsgUrlUnsupported = "url must have // after the repository url. example: https://github.com/kubernetes-sigs/kustomize//examples" 27 | errMsgUrlColon = "first path segment in URL cannot contain colon" 28 | ) 29 | 30 | type KustomizeRemote struct { 31 | raw string 32 | 33 | Scheme string 34 | User string 35 | Password string 36 | Host string 37 | Port string 38 | RepoPath string 39 | 40 | FilePath string 41 | 42 | Ref string 43 | Submodules bool 44 | Timeout time.Duration 45 | } 46 | 47 | func (g *KustomizeRemote) CloneUrl() string { 48 | sb := strings.Builder{} 49 | if g.Scheme != "" { 50 | sb.WriteString(fmt.Sprintf("%s://", g.Scheme)) 51 | } 52 | if g.User != "" { 53 | sb.WriteString(g.User) 54 | if g.Password != "" { 55 | sb.WriteString(fmt.Sprintf(":%s", g.Password)) 56 | } 57 | sb.Write([]byte(UserDelimiter)) 58 | } 59 | 60 | sb.WriteString(g.Host) 61 | if g.Port != "" { 62 | sb.WriteString(fmt.Sprintf(":%s", g.Port)) 63 | } 64 | if g.Scheme == "" { 65 | sb.WriteString(":") 66 | } else { 67 | sb.WriteString("/") 68 | } 69 | 70 | sb.WriteString(g.RepoPath) 71 | return sb.String() 72 | } 73 | 74 | func (g *KustomizeRemote) Path() string { 75 | return g.FilePath 76 | } 77 | 78 | func (g *KustomizeRemote) parseQuery() error { 79 | _, query, _ := strings.Cut(g.raw, "?") 80 | values, err := url.ParseQuery(query) 81 | 82 | if err != nil { 83 | return err 84 | } 85 | 86 | // if empty, it means we checkout the default branch 87 | version := values.Get(QueryStringVersion) 88 | ref := values.Get(QueryStringRef) 89 | // ref has higher priority per kustomize doc 90 | if ref != "" { 91 | version = ref 92 | } 93 | 94 | duration := defaultTimeout 95 | timeoutString := values.Get(QueryStringTimeout) 96 | if timeoutString != "" { 97 | timeoutInt, sErr := strconv.Atoi(timeoutString) 98 | if sErr == nil { 99 | duration = time.Duration(timeoutInt) * time.Second 100 | } else { 101 | t, sErr := time.ParseDuration(timeoutString) 102 | if sErr == nil { 103 | duration = t 104 | } 105 | } 106 | } 107 | 108 | cloneSubmodules := defaultCloneSubmodule 109 | submodule := values.Get(QueryStringSubmodules) 110 | if submodule != "" { 111 | v, pErr := strconv.ParseBool(submodule) 112 | if pErr == nil { 113 | cloneSubmodules = v 114 | } 115 | } 116 | 117 | g.Ref = version 118 | g.Submodules = cloneSubmodules 119 | g.Timeout = duration 120 | 121 | return nil 122 | } 123 | 124 | func (g *KustomizeRemote) parse() error { 125 | parsed, err := url.Parse(g.raw) 126 | if err != nil { 127 | if strings.Contains(err.Error(), errMsgUrlColon) { 128 | return g.parseSCPStyle() 129 | } 130 | return err 131 | } 132 | 133 | g.Scheme, g.User, g.Host = parsed.Scheme, parsed.User.Username(), parsed.Host 134 | p, ok := parsed.User.Password() 135 | if ok { 136 | g.Password = p 137 | } 138 | 139 | err = g.parseQuery() 140 | if err != nil { 141 | return fmt.Errorf("parsing query parameters in package url: %s: %w", g.raw, err) 142 | } 143 | 144 | return g.parsePath(parsed.Path) 145 | } 146 | 147 | func (g *KustomizeRemote) parseSCPStyle() error { 148 | // example git@github.com:owner/repo 149 | cIndex := strings.Index(g.raw, SCPDelimiter) 150 | if cIndex == -1 { 151 | return fmt.Errorf("not a valid SCP style URL") 152 | } 153 | 154 | uIndex := strings.Index(g.raw[:cIndex], UserDelimiter) 155 | if uIndex != -1 { 156 | g.User = g.raw[:uIndex] 157 | } 158 | g.Host = g.raw[uIndex+1 : cIndex] 159 | err := g.parseQuery() 160 | if err != nil { 161 | return fmt.Errorf("parsing query parameters in package url: %s: %w", g.raw, err) 162 | } 163 | 164 | pathEnd := len(g.raw) 165 | qIndex := strings.Index(g.raw, "?") 166 | if qIndex != -1 { 167 | pathEnd = qIndex 168 | } 169 | return g.parsePath(g.raw[cIndex+1 : pathEnd]) 170 | } 171 | 172 | func (g *KustomizeRemote) parsePath(path string) error { 173 | // example kubernetes-sigs/kustomize//examples/multibases/dev/ 174 | index := strings.Index(path, RepoUrlDelimiter) 175 | if index == -1 { 176 | return fmt.Errorf(errMsgUrlUnsupported) 177 | } 178 | 179 | g.RepoPath = strings.TrimPrefix(path[:index], "/") 180 | g.FilePath = strings.TrimSuffix(path[index+2:], "/") 181 | return nil 182 | } 183 | 184 | func NewKustomizeRemote(uri string) (*KustomizeRemote, error) { 185 | r := &KustomizeRemote{raw: uri} 186 | return r, r.parse() 187 | } 188 | -------------------------------------------------------------------------------- /pkg/controllers/localbuild/gitea.go: -------------------------------------------------------------------------------- 1 | package localbuild 2 | 3 | import ( 4 | "context" 5 | "embed" 6 | "encoding/base64" 7 | "fmt" 8 | "github.com/cnoe-io/idpbuilder/pkg/k8s" 9 | "net/http" 10 | 11 | "github.com/cnoe-io/idpbuilder/api/v1alpha1" 12 | "github.com/cnoe-io/idpbuilder/pkg/util" 13 | corev1 "k8s.io/api/core/v1" 14 | k8serrors "k8s.io/apimachinery/pkg/api/errors" 15 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 16 | "k8s.io/apimachinery/pkg/runtime" 17 | "k8s.io/apimachinery/pkg/runtime/schema" 18 | "k8s.io/apimachinery/pkg/types" 19 | ctrl "sigs.k8s.io/controller-runtime" 20 | "sigs.k8s.io/controller-runtime/pkg/client" 21 | "sigs.k8s.io/controller-runtime/pkg/log" 22 | ) 23 | 24 | //go:embed resources/gitea/k8s/* 25 | var installGiteaFS embed.FS 26 | 27 | func RawGiteaInstallResources(templateData any, config v1alpha1.PackageCustomization, scheme *runtime.Scheme) ([][]byte, error) { 28 | return k8s.BuildCustomizedManifests(config.FilePath, "resources/gitea/k8s", installGiteaFS, scheme, templateData) 29 | } 30 | 31 | func (r *LocalbuildReconciler) newGiteaAdminSecret(password string) corev1.Secret { 32 | obj := util.GiteaAdminSecretObject() 33 | obj.StringData = map[string]string{ 34 | "username": v1alpha1.GiteaAdminUserName, 35 | "password": password, 36 | } 37 | return obj 38 | } 39 | 40 | func (r *LocalbuildReconciler) ReconcileGitea(ctx context.Context, req ctrl.Request, resource *v1alpha1.Localbuild) (ctrl.Result, error) { 41 | logger := log.FromContext(ctx, "installer", "gitea") 42 | gitea := EmbeddedInstallation{ 43 | name: "Gitea", 44 | resourcePath: "resources/gitea/k8s", 45 | resourceFS: installGiteaFS, 46 | namespace: util.GiteaNamespace, 47 | monitoredResources: map[string]schema.GroupVersionKind{ 48 | "my-gitea": { 49 | Group: "apps", 50 | Version: "v1", 51 | Kind: "Deployment", 52 | }, 53 | }, 54 | } 55 | 56 | sec := util.GiteaAdminSecretObject() 57 | err := r.Client.Get(ctx, types.NamespacedName{ 58 | Namespace: sec.GetNamespace(), 59 | Name: sec.GetName(), 60 | }, &sec) 61 | 62 | if err != nil { 63 | if k8serrors.IsNotFound(err) { 64 | genPassword, err := util.GeneratePassword() 65 | if err != nil { 66 | return ctrl.Result{}, fmt.Errorf("generating gitea password: %w", err) 67 | } 68 | 69 | giteaCreds := r.newGiteaAdminSecret(genPassword) 70 | if err != nil { 71 | return ctrl.Result{}, fmt.Errorf("generating gitea admin secret: %w", err) 72 | } 73 | gitea.unmanagedResources = []client.Object{&giteaCreds} 74 | sec = giteaCreds 75 | } else { 76 | return ctrl.Result{}, fmt.Errorf("getting gitea secret: %w", err) 77 | } 78 | } 79 | 80 | v, ok := resource.Spec.PackageConfigs.CorePackageCustomization[v1alpha1.GiteaPackageName] 81 | if ok { 82 | gitea.customization = v 83 | } 84 | 85 | if result, err := gitea.Install(ctx, resource, r.Client, r.Scheme, r.Config); err != nil { 86 | return result, err 87 | } 88 | 89 | baseUrl := util.GiteaBaseUrl(r.Config) 90 | 91 | // need this to ensure gitrepository controller can reach the api endpoint. 92 | logger.V(1).Info("checking gitea api endpoint", "url", baseUrl) 93 | c := util.GetHttpClient() 94 | resp, err := c.Get(baseUrl) 95 | if err != nil { 96 | return ctrl.Result{}, err 97 | } 98 | if resp != nil { 99 | resp.Body.Close() 100 | if resp.StatusCode != http.StatusOK { 101 | logger.V(1).Info("gitea manifests installed successfully. endpoint not ready", "statusCode", resp.StatusCode) 102 | return ctrl.Result{RequeueAfter: errRequeueTime}, nil 103 | } 104 | } 105 | 106 | err = r.setGiteaToken(ctx, sec, baseUrl) 107 | if err != nil { 108 | return ctrl.Result{}, fmt.Errorf("creating gitea token: %w", err) 109 | } 110 | 111 | resource.Status.Gitea.ExternalURL = baseUrl 112 | resource.Status.Gitea.InternalURL = util.GiteaBaseUrl(r.Config) 113 | resource.Status.Gitea.AdminUserSecretName = util.GiteaAdminSecret 114 | resource.Status.Gitea.AdminUserSecretNamespace = util.GiteaNamespace 115 | resource.Status.Gitea.Available = true 116 | return ctrl.Result{}, nil 117 | } 118 | 119 | func (r *LocalbuildReconciler) setGiteaToken(ctx context.Context, secret corev1.Secret, baseUrl string) error { 120 | _, ok := secret.Data[util.GiteaAdminTokenFieldName] 121 | if ok { 122 | return nil 123 | } 124 | 125 | u := unstructured.Unstructured{} 126 | u.SetName(util.GiteaAdminSecret) 127 | u.SetNamespace(util.GiteaNamespace) 128 | u.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Secret")) 129 | 130 | user, ok := secret.Data["username"] 131 | if !ok { 132 | return fmt.Errorf("username field not found in gitea secret") 133 | } 134 | 135 | pass, ok := secret.Data["password"] 136 | if !ok { 137 | return fmt.Errorf("password field not found in gitea secret") 138 | } 139 | 140 | t, err := util.GetGiteaToken(ctx, baseUrl, string(user), string(pass)) 141 | if err != nil { 142 | return fmt.Errorf("getting gitea token: %w", err) 143 | } 144 | 145 | token := base64.StdEncoding.EncodeToString([]byte(t)) 146 | err = unstructured.SetNestedField(u.Object, token, "data", util.GiteaAdminTokenFieldName) 147 | if err != nil { 148 | return fmt.Errorf("setting gitea token field: %w", err) 149 | } 150 | 151 | return r.Client.Patch(ctx, &u, client.Apply, client.ForceOwnership, client.FieldOwner(v1alpha1.FieldManager)) 152 | } 153 | -------------------------------------------------------------------------------- /pkg/controllers/resources/idpbuilder.cnoe.io_custompackages.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.15.0 7 | name: custompackages.idpbuilder.cnoe.io 8 | spec: 9 | group: idpbuilder.cnoe.io 10 | names: 11 | kind: CustomPackage 12 | listKind: CustomPackageList 13 | plural: custompackages 14 | singular: custompackage 15 | scope: Namespaced 16 | versions: 17 | - name: v1alpha1 18 | schema: 19 | openAPIV3Schema: 20 | properties: 21 | apiVersion: 22 | description: |- 23 | APIVersion defines the versioned schema of this representation of an object. 24 | Servers should convert recognized schemas to the latest internal value, and 25 | may reject unrecognized values. 26 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 27 | type: string 28 | kind: 29 | description: |- 30 | Kind is a string value representing the REST resource this object represents. 31 | Servers may infer this from the endpoint the client submits requests to. 32 | Cannot be updated. 33 | In CamelCase. 34 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 35 | type: string 36 | metadata: 37 | type: object 38 | spec: 39 | description: CustomPackageSpec controls the installation of the custom 40 | applications. 41 | properties: 42 | argoCD: 43 | properties: 44 | applicationFile: 45 | description: ApplicationFile specifies the absolute path to the 46 | ArgoCD application file 47 | type: string 48 | name: 49 | type: string 50 | namespace: 51 | type: string 52 | type: 53 | enum: 54 | - Application 55 | - ApplicationSet 56 | type: string 57 | required: 58 | - applicationFile 59 | - name 60 | - namespace 61 | - type 62 | type: object 63 | gitServerAuthSecretRef: 64 | properties: 65 | name: 66 | type: string 67 | namespace: 68 | type: string 69 | required: 70 | - name 71 | - namespace 72 | type: object 73 | gitServerURL: 74 | description: |- 75 | GitServerURL specifies the base URL for the git server for API calls. 76 | for example, https://gitea.cnoe.localtest.me:8443 77 | type: string 78 | internalGitServeURL: 79 | description: |- 80 | InternalGitServeURL specifies the base URL for the git server accessible within the cluster. 81 | for example, http://my-gitea-http.gitea.svc.cluster.local:3000 82 | type: string 83 | remoteRepository: 84 | description: RemoteRepositorySpec specifies information about remote 85 | repositories. 86 | properties: 87 | cloneSubmodules: 88 | type: boolean 89 | path: 90 | type: string 91 | ref: 92 | description: Ref specifies the specific ref supported by git fetch 93 | type: string 94 | url: 95 | description: Url specifies the url to the repository containing 96 | the ArgoCD application file 97 | type: string 98 | required: 99 | - cloneSubmodules 100 | - path 101 | - ref 102 | - url 103 | type: object 104 | replicate: 105 | default: false 106 | description: Replicate specifies whether to replicate remote or local 107 | contents to the local gitea server. 108 | type: boolean 109 | required: 110 | - gitServerAuthSecretRef 111 | - gitServerURL 112 | - internalGitServeURL 113 | - remoteRepository 114 | - replicate 115 | type: object 116 | status: 117 | properties: 118 | gitRepositoryRefs: 119 | items: 120 | properties: 121 | apiVersion: 122 | type: string 123 | kind: 124 | type: string 125 | name: 126 | type: string 127 | namespace: 128 | type: string 129 | uid: 130 | type: string 131 | type: object 132 | type: array 133 | synced: 134 | description: |- 135 | A Custom package is considered synced when the in-cluster repository url is set as the repository URL 136 | This only applies for a package that references local directories 137 | type: boolean 138 | type: object 139 | type: object 140 | served: true 141 | storage: true 142 | subresources: 143 | status: {} 144 | -------------------------------------------------------------------------------- /api/v1alpha1/localbuild_types.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/cnoe-io/idpbuilder/globals" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | ) 9 | 10 | const ( 11 | // LastObservedCLIStartTimeAnnotation indicates when the controller acted on a resource. 12 | LastObservedCLIStartTimeAnnotation = "cnoe.io/last-observed-cli-start-time" 13 | // CliStartTimeAnnotation indicates when the CLI was invoked. 14 | CliStartTimeAnnotation = "cnoe.io/cli-start-time" 15 | // PackagePriorityAnnotation indicates the priority of a package (higher = wins conflicts). 16 | PackagePriorityAnnotation = "cnoe.io/package-priority" 17 | // PackageSourcePathAnnotation indicates the source path of a package. 18 | PackageSourcePathAnnotation = "cnoe.io/package-source-path" 19 | FieldManager = "idpbuilder" 20 | // If GetSecretLabelKey is set to GetSecretLabelValue on a kubernetes secret, secret key and values can be used by the get command. 21 | CLISecretLabelKey = "cnoe.io/cli-secret" 22 | CLISecretLabelValue = "true" 23 | PackageNameLabelKey = "cnoe.io/package-name" 24 | PackageTypeLabelKey = "cnoe.io/package-type" 25 | PackageTypeLabelCore = "core" 26 | PackageTypeLabelCustom = "custom" 27 | 28 | ArgoCDPackageName = "argocd" 29 | GiteaPackageName = "gitea" 30 | IngressNginxPackageName = "nginx" 31 | ) 32 | 33 | // ArgoPackageConfigSpec Allows for configuration of the ArgoCD Installation. 34 | // If no fields are specified then the binary embedded resources will be used to install ArgoCD. 35 | type ArgoPackageConfigSpec struct { 36 | // Enabled controls whether to install ArgoCD. 37 | Enabled bool `json:"enabled,omitempty"` 38 | } 39 | 40 | // EmbeddedArgoApplicationsPackageConfigSpec Controls the installation of the embedded argo applications. 41 | type EmbeddedArgoApplicationsPackageConfigSpec struct { 42 | // Enabled controls whether to install the embedded argo applications and the associated GitServer 43 | Enabled bool `json:"enabled,omitempty"` 44 | } 45 | 46 | type PackageConfigsSpec struct { 47 | Argo ArgoPackageConfigSpec `json:"argoPackageConfigs,omitempty"` 48 | EmbeddedArgoApplications EmbeddedArgoApplicationsPackageConfigSpec `json:"embeddedArgoApplicationsPackageConfigs,omitempty"` 49 | CustomPackageFiles []string `json:"customPackageFiles,omitempty"` 50 | CustomPackageDirs []string `json:"customPackageDirs,omitempty"` 51 | CustomPackageUrls []string `json:"customPackageUrls,omitempty"` 52 | // +kubebuilder:validation:Optional 53 | CorePackageCustomization map[string]PackageCustomization `json:"packageCustomization,omitempty"` 54 | } 55 | 56 | // BuildCustomizationSpec fields cannot change once a cluster is created 57 | type BuildCustomizationSpec struct { 58 | Protocol string `json:"protocol,omitempty"` 59 | Host string `json:"host,omitempty"` 60 | IngressHost string `json:"ingressHost,omitempty"` 61 | Port string `json:"port,omitempty"` 62 | UsePathRouting bool `json:"usePathRouting,omitempty"` 63 | SelfSignedCert string `json:"selfSignedCert,omitempty"` 64 | StaticPassword bool `json:"staticPassword,omitempty"` 65 | } 66 | 67 | type LocalbuildSpec struct { 68 | PackageConfigs PackageConfigsSpec `json:"packageConfigs,omitempty"` 69 | BuildCustomization BuildCustomizationSpec `json:"buildCustomization,omitempty"` 70 | } 71 | 72 | // PackageCustomization defines how packages are customized 73 | type PackageCustomization struct { 74 | // Name is the name of the package to be customized. e.g. argocd 75 | Name string `json:"name,omitempty'"` 76 | // FilePath is the absolute file path to a YAML file that contains Kubernetes manifests. 77 | FilePath string `json:"filePath,omitempty"` 78 | } 79 | 80 | type LocalbuildStatus struct { 81 | // ObservedGeneration is the 'Generation' of the Service that was last processed by the controller. 82 | // +optional 83 | ObservedGeneration int64 `json:"observedGeneration,omitempty"` 84 | ArgoCD ArgoCDStatus `json:"ArgoCD,omitempty"` 85 | Nginx NginxStatus `json:"nginx,omitempty"` 86 | Gitea GiteaStatus `json:"gitea,omitempty"` 87 | } 88 | 89 | type GiteaStatus struct { 90 | Available bool `json:"available,omitempty"` 91 | ExternalURL string `json:"externalURL,omitempty"` 92 | InternalURL string `json:"internalURL,omitempty"` 93 | AdminUserSecretName string `json:"adminUserSecretNameecret,omitempty"` 94 | AdminUserSecretNamespace string `json:"adminUserSecretNamespace,omitempty"` 95 | } 96 | 97 | type ArgoCDStatus struct { 98 | Available bool `json:"available,omitempty"` 99 | AppsCreated bool `json:"appsCreated,omitempty"` 100 | } 101 | 102 | type NginxStatus struct { 103 | Available bool `json:"available,omitempty"` 104 | } 105 | 106 | // +kubebuilder:object:root=true 107 | // +kubebuilder:subresource:status 108 | // +kubebuilder:resource:path=localbuilds,scope=Cluster 109 | type Localbuild struct { 110 | metav1.TypeMeta `json:",inline"` 111 | metav1.ObjectMeta `json:"metadata,omitempty"` 112 | 113 | Spec LocalbuildSpec `json:"spec,omitempty"` 114 | Status LocalbuildStatus `json:"status,omitempty"` 115 | } 116 | 117 | func (l *Localbuild) GetArgoProjectName() string { 118 | return fmt.Sprintf("%s-%s-gitserver", globals.ProjectName, l.Name) 119 | } 120 | 121 | func (l *Localbuild) GetArgoApplicationName(name string) string { 122 | return fmt.Sprintf("%s-%s-gitserver-%s", globals.ProjectName, l.Name, name) 123 | } 124 | 125 | // +kubebuilder:object:root=true 126 | type LocalbuildList struct { 127 | metav1.TypeMeta `json:",inline"` 128 | metav1.ListMeta `json:"metadata,omitempty"` 129 | Items []Localbuild `json:"items"` 130 | } 131 | --------------------------------------------------------------------------------