diff --git a/.github/workflows/check-codegen.yml b/.github/workflows/check-codegen.yml new file mode 100644 index 00000000..e501731e --- /dev/null +++ b/.github/workflows/check-codegen.yml @@ -0,0 +1,39 @@ +name: Check Codegen + +on: + pull_request: + paths-ignore: + - 'docs/**' + - '**/*.md' + +jobs: + check-codegen: + name: Run on Ubuntu + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + + - name: Run make generate + run: make generate + + - name: Run make docs + run: make docs + + - name: Run make helm + run: make helm + + - name: Run fmt + run: make fmt + + - name: Compare the expected and actual generated/* directories + run: | + if [ "$(git diff | wc -l)" -gt "0" ]; then + echo "Detected uncommitted changes after build. Consider running 'make generate && make docs && make helm'." + echo "See status below:" + git diff + exit 1 + fi diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml deleted file mode 100644 index 62f4ca0f..00000000 --- a/.github/workflows/checks.yaml +++ /dev/null @@ -1,65 +0,0 @@ -################################################################################ -# This file is AUTOGENERATED with # -# Edit Makefile.maker.yaml instead. # -################################################################################ - -# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company -# SPDX-License-Identifier: Apache-2.0 - -name: Checks -"on": - push: - branches: - - main - pull_request: - branches: - - '*' - workflow_dispatch: {} -permissions: - checks: write - contents: read -jobs: - checks: - name: Checks - runs-on: ubuntu-latest - steps: - - name: Check out code - uses: actions/checkout@v4 - - name: Set up Go - uses: actions/setup-go@v5 - with: - check-latest: true - go-version: 1.25.1 - - name: Cache golangci-lint analysis - uses: actions/cache@v4 - with: - path: ~/.cache/golangci-lint - key: ${{ runner.os }}-golangci-lint-${{ hashFiles('go.sum', '.golangci.yaml') }} - restore-keys: | - ${{ runner.os }}-golangci-lint- - - name: Run prepare make target - run: make generate - - name: Run golangci-lint - run: | - # FIXME: Exclude cisco nx-os provider from golangci-lint as it includes - # a lot of generated code that will exceed the runners's constraints. - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.5.0 - go list -f '{{.Dir}}' ./... | grep -v nxos/ | xargs $(go env GOPATH)/bin/golangci-lint run - - name: Run shellcheck - uses: ludeeus/action-shellcheck@2.0.0 - - name: Dependency Licenses Review - run: make check-dependency-licenses - - name: Check for spelling errors - uses: reviewdog/action-misspell@v1 - with: - exclude: ./vendor/* - fail_on_error: true - github_token: ${{ secrets.GITHUB_TOKEN }} - ignore: importas - reporter: github-check - - name: Check if source code files have license header - run: make check-addlicense - - name: Install govulncheck - run: go install golang.org/x/vuln/cmd/govulncheck@latest - - name: Run govulncheck - run: govulncheck -format text ./... diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml deleted file mode 100644 index e1d61e74..00000000 --- a/.github/workflows/ci.yaml +++ /dev/null @@ -1,56 +0,0 @@ -################################################################################ -# This file is AUTOGENERATED with # -# Edit Makefile.maker.yaml instead. # -################################################################################ - -# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company -# SPDX-License-Identifier: Apache-2.0 - -name: CI -"on": - push: - branches: - - main - paths-ignore: - - '**.md' - pull_request: - branches: - - '*' - paths-ignore: - - '**.md' - workflow_dispatch: {} -permissions: - contents: read -jobs: - build: - name: Build - runs-on: ubuntu-latest - steps: - - name: Check out code - uses: actions/checkout@v4 - - name: Set up Go - uses: actions/setup-go@v5 - with: - check-latest: true - go-version: 1.25.1 - - name: Run prepare make target - run: make generate - - name: Build all binaries - run: make build-all - test: - name: Test - needs: - - build - runs-on: ubuntu-latest - steps: - - name: Check out code - uses: actions/checkout@v4 - - name: Set up Go - uses: actions/setup-go@v5 - with: - check-latest: true - go-version: 1.25.1 - - name: Run prepare make target - run: make generate - - name: Run tests and generate coverage report - run: make build/cover.out diff --git a/.github/workflows/goreleaser.yaml b/.github/workflows/goreleaser.yaml index 8daecb73..b8baf0f6 100644 --- a/.github/workflows/goreleaser.yaml +++ b/.github/workflows/goreleaser.yaml @@ -1,11 +1,3 @@ -################################################################################ -# This file is AUTOGENERATED with # -# Edit Makefile.maker.yaml instead. # -################################################################################ - -# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company -# SPDX-License-Identifier: Apache-2.0 - name: goreleaser "on": push: @@ -16,7 +8,7 @@ permissions: packages: write jobs: release: - name: goreleaser + name: Run on Ubuntu runs-on: ubuntu-latest steps: - name: Check out code diff --git a/.github/workflows/kustomize-validation.yml b/.github/workflows/kustomize-validation.yml new file mode 100644 index 00000000..9c2c40b1 --- /dev/null +++ b/.github/workflows/kustomize-validation.yml @@ -0,0 +1,24 @@ +name: Kustomize Validation + +on: + pull_request: + types: [ assigned, opened, synchronize, reopened ] + paths-ignore: + - 'docs/**' + - '**/*.md' + +jobs: + kustomize-validation: + name: Run on Ubuntu + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v5 + + - name: Install Kustomize + run: | + make kustomize + mv ./bin/kustomize /usr/local/bin + + - run: | + ./hack/validate-kustomize.sh diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..e97024d1 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,26 @@ +name: Lint + +on: + pull_request: + paths-ignore: + - 'docs/**' + - '**/*.md' + +jobs: + lint: + name: Run on Ubuntu + runs-on: ubuntu-latest + steps: + - name: Clone the code + uses: actions/checkout@v5 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Run linter + run: | + make golangci-lint + cp ./bin/golangci-lint /usr/local/bin/ + go list -f '{{.Dir}}' ./... | grep -v nxos/ | xargs golangci-lint run diff --git a/.github/workflows/publish-chart.yaml b/.github/workflows/publish-chart.yaml new file mode 100644 index 00000000..b4d6a726 --- /dev/null +++ b/.github/workflows/publish-chart.yaml @@ -0,0 +1,85 @@ +name: Release Helm Chart + +on: + release: + types: + - published + push: + branches: + - main + tags: + - 'v*.*.*' + paths-ignore: + - 'docs/**' + - '**/*.md' + pull_request: + branches: + - main + paths-ignore: + - 'docs/**' + - '**/*.md' + types: [labeled, opened, synchronize, reopened] + +jobs: + helm-chart: + name: Run on Ubuntu + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + if: | + github.event_name == 'push' || + (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'ok-to-charts')) || + (github.event_name == 'release' && github.event.action == 'published') + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Set up Helm + uses: azure/setup-helm@v4 + with: + version: v3.17.0 + + - name: Determine chart version + id: chart_version + run: | + if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" == "refs/heads/main" ]]; then + # Use SHA for main branch + CHART_VERSION="0.0.0-$(echo ${{ github.sha }} | cut -c1-7)" + elif [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + # Use tag version (strip 'v' prefix) + CHART_VERSION="${GITHUB_REF#refs/tags/v}" + else + # Use PR SHA for dry run + CHART_VERSION="0.0.0-$(echo ${{ github.sha }} | cut -c1-7)" + fi + echo "version=$CHART_VERSION" >> $GITHUB_OUTPUT + + - name: Install Kustomize + run: | + make kustomize + mv ./bin/kustomize /usr/local/bin + + - name: Package Helm chart with crds folder in template + run: | + helm package charts/network-operator --version ${{ steps.chart_version.outputs.version }}-crds + + - name: Prepare CRDs folder + run: | + mkdir -p charts/network-operator/crds + kustomize build config/default | yq ea 'select(.kind == "CustomResourceDefinition")' > charts/network-operator/crds/crds.yaml + rm -rf charts/network-operator/templates/crd + + - name: Package Helm chart with removed crds folder from template folder + run: | + helm package charts/network-operator --version ${{ steps.chart_version.outputs.version }} + + - name: Log in to GitHub Container Registry + run: | + echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io -u ${{ github.actor }} --password-stdin + + - name: Push Helm chart to GHCR + run: | + helm push network-operator-${{ steps.chart_version.outputs.version }}.tgz oci://ghcr.io/${{ github.repository_owner }}/charts + helm push network-operator-${{ steps.chart_version.outputs.version }}-crds.tgz oci://ghcr.io/${{ github.repository_owner }}/charts diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml new file mode 100644 index 00000000..7aea3479 --- /dev/null +++ b/.github/workflows/publish-docker.yml @@ -0,0 +1,92 @@ +name: Build and Publish Docker Image + +on: + release: + types: + - published + push: + branches: + - main + tags: + - v* + paths-ignore: + - 'docs/**' + - '**/*.md' + pull_request: + paths-ignore: + - 'docs/**' + - '**/*.md' + types: [labeled, unlabeled, opened, synchronize, reopened] + +jobs: + buildAndPush: + name: Run on Ubuntu + strategy: + matrix: + image: + - name: network-operator-controller-manager + target: manager + permissions: + contents: read + packages: write + # Condition: Run on push to main, published release, OR PR with 'ok-to-image' label + if: | + github.event_name == 'push' || + (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'ok-to-image')) || + (github.event_name == 'release' && github.event.action == 'published') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - uses: docker/metadata-action@v5 + id: meta + with: + images: | + ghcr.io/${{ github.repository_owner }}/${{ matrix.image.name }} + tags: | + type=semver,pattern={{version}} + type=schedule + type=ref,event=branch + type=ref,event=tag + type=ref,event=pr + type=sha + flavor: | + latest=${{ github.ref == 'refs/heads/main' }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: all + + - name: Set up Docker Buildx + timeout-minutes: 5 + uses: docker/setup-buildx-action@v3 + with: + version: latest + + - name: Login to GHCR + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Get and Save Date + id: date_step + run: echo "current_datetime=$(date +'%Y-%m-%d %H:%M:%S')" >> "$GITHUB_OUTPUT" + + - name: Build and push + timeout-minutes: 40 + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + target: ${{ matrix.image.target }} + build-args: | + VERSION=${{ steps.meta.outputs.version }} + GIT_COMMIT=${{ github.sha }} + BUILD_DATE="${{ steps.date_step.outputs.current_datetime }}" diff --git a/.github/workflows/reuse.yaml b/.github/workflows/reuse.yaml index 7597d554..4d5022b7 100644 --- a/.github/workflows/reuse.yaml +++ b/.github/workflows/reuse.yaml @@ -1,6 +1,3 @@ -# SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors -# SPDX-License-Identifier: Apache-2.0 - name: REUSE Compliance on: push: @@ -14,9 +11,10 @@ permissions: contents: read jobs: test: - name: Check + name: Run on Ubuntu runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: REUSE Compliance Check uses: fsfe/reuse-action@v5 diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml deleted file mode 100644 index 79d72896..00000000 --- a/.github/workflows/stale.yaml +++ /dev/null @@ -1,32 +0,0 @@ -# SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors -# SPDX-License-Identifier: Apache-2.0 - -name: Close inactive issues -on: - schedule: - - cron: "35 1 * * *" - -jobs: - close-issues: - runs-on: ubuntu-latest - permissions: - issues: write - pull-requests: write - steps: - - uses: actions/stale@v9 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - days-before-issue-stale: 90 - days-before-issue-close: 14 - days-before-pr-stale: 45 - days-before-pr-close: 14 - stale-issue-label: "stale" - stale-issue-message: "This issue is stale because it has been open for 90 days with no activity." - close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale." - stale-pr-label: 'stale' - stale-pr-message: "This PR is stale because it has been open for 45 days with no activity." - close-pr-message: "This PR was closed because it has been inactive for 14 days since being marked as stale." - exempt-issue-labels: "pinned,security,backlog,bug" - exempt-pr-labels: "pinned,security,backlog,bug" - exempt-draft-pr: true - diff --git a/.github/workflows/test-chart.yml b/.github/workflows/test-chart.yml index d41d461f..60e32365 100644 --- a/.github/workflows/test-chart.yml +++ b/.github/workflows/test-chart.yml @@ -1,95 +1,62 @@ -# SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors -# SPDX-License-Identifier: Apache-2.0 +name: Test Chart + +permissions: + contents: read -name: Test on: - push: - branches: - - main - paths-ignore: - - '**.md' pull_request: - branches: - - '*' paths-ignore: - - '**.md' - workflow_dispatch: {} -permissions: - contents: read + - 'docs/**' + - '**/*.md' + jobs: test-chart: - name: Chart + name: Run on Ubuntu runs-on: ubuntu-latest steps: - - name: Check out code - uses: actions/checkout@v4 - - name: Set up Go + - name: Clone the code + uses: actions/checkout@v5 + + - name: Setup Go uses: actions/setup-go@v5 with: - check-latest: true - go-version: 1.25.1 - - name: Fetch latest kubectl version - id: kubectl - run: | - KUBECTL_VERSION=$(curl -sL https://dl.k8s.io/release/stable.txt) - echo "version=$KUBECTL_VERSION" >> $GITHUB_OUTPUT - - name: Fetch latest kind version - id: kind + go-version-file: go.mod + + - uses: azure/setup-helm@v4.3.0 + with: + version: 'v3.17.0' + + - name: Lint Helm Chart run: | - KIND_VERSION=$(curl -s https://api.github.com/repos/kubernetes-sigs/kind/releases/latest | grep '"tag_name":' | cut -d'"' -f4) - echo "version=$KIND_VERSION" >> $GITHUB_OUTPUT - - name: Create k8s kind cluster + helm lint ./charts/network-operator + + - name: Create k8s Kind Cluster uses: helm/kind-action@v1 with: - version: ${{ steps.kind.outputs.version }} - cluster_name: kind - kubectl_version: ${{ steps.kubectl.outputs.version }} + cluster_name: 'kind' + - name: Prepare network-operator run: | - go mod tidy + go mod download make docker-build IMG=network-operator:v0.1.0 kind load docker-image network-operator:v0.1.0 - - name: Install Helm + + - name: Install cert-manager via Helm run: | - curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash - - name: Verify Helm installation - run: helm version - - name: Lint Helm Chart + helm repo add jetstack https://charts.jetstack.io + helm repo update + helm install cert-manager jetstack/cert-manager --namespace cert-manager --create-namespace --set installCRDs=true + + - name: Wait for cert-manager to be ready run: | - helm lint ./charts/network-operator -# TODO: Uncomment if cert-manager is enabled -# - name: Install cert-manager via Helm -# run: | -# helm repo add jetstack https://charts.jetstack.io -# helm repo update -# helm install cert-manager jetstack/cert-manager --namespace cert-manager --create-namespace --set installCRDs=true -# - name: Wait for cert-manager to be ready -# run: | -# kubectl wait --namespace cert-manager --for=condition=available --timeout=300s deployment/cert-manager -# kubectl wait --namespace cert-manager --for=condition=available --timeout=300s deployment/cert-manager-cainjector -# kubectl wait --namespace cert-manager --for=condition=available --timeout=300s deployment/cert-manager-webhook -# TODO: Uncomment if Prometheus is enabled -# - name: Install Prometheus Operator CRDs -# run: | -# helm repo add prometheus-community https://prometheus-community.github.io/helm-charts -# helm repo update -# helm install prometheus-crds prometheus-community/prometheus-operator-crds -# - name: Install Prometheus via Helm -# run: | -# helm repo add prometheus-community https://prometheus-community.github.io/helm-charts -# helm repo update -# helm install prometheus prometheus-community/prometheus --namespace monitoring --create-namespace -# - name: Wait for Prometheus to be ready -# run: | -# kubectl wait --namespace monitoring --for=condition=available --timeout=300s deployment/prometheus-server + kubectl wait --namespace cert-manager --for=condition=available --timeout=300s deployment/cert-manager + kubectl wait --namespace cert-manager --for=condition=available --timeout=300s deployment/cert-manager-cainjector + kubectl wait --namespace cert-manager --for=condition=available --timeout=300s deployment/cert-manager-webhook + - name: Install Helm chart for project run: | - helm dependency build ./charts/network-operator - helm install network-operator ./charts/network-operator --create-namespace --namespace network-operator-system + helm install my-release ./charts/network-operator --create-namespace --namespace network-operator-system + - name: Check Helm release status run: | - helm status network-operator --namespace network-operator-system -# TODO: Uncomment if prometheus.enabled is set to true to confirm that the ServiceMonitor gets created -# - name: Check Presence of ServiceMonitor -# run: | -# kubectl wait --namespace network-operator-system --for=jsonpath='{.kind}'=ServiceMonitor servicemonitor/network-operator-controller-manager-metrics-monitor + helm status my-release --namespace network-operator-system diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index a9496a9f..030b0ace 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -1,50 +1,31 @@ -# SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors -# SPDX-License-Identifier: Apache-2.0 +name: E2E Tests -name: Test on: - push: - branches: - - main - paths-ignore: - - '**.md' pull_request: - branches: - - '*' + types: [ assigned, opened, synchronize, reopened ] paths-ignore: - - '**.md' - workflow_dispatch: {} -permissions: - contents: read + - 'docs/**' + - '**/*.md' + jobs: test-e2e: - name: E2E + name: Run on Ubuntu runs-on: ubuntu-latest steps: - - name: Check out code - uses: actions/checkout@v4 - - name: Set up Go + - name: Clone the code + uses: actions/checkout@v5 + + - name: Setup Go uses: actions/setup-go@v5 with: - check-latest: true - go-version: 1.25.1 - - name: Fetch latest kubectl version - id: kubectl - run: | - KUBECTL_VERSION=$(curl -sL https://dl.k8s.io/release/stable.txt) - echo "version=$KUBECTL_VERSION" >> $GITHUB_OUTPUT - - name: Fetch latest kind version - id: kind - run: | - KIND_VERSION=$(curl -s https://api.github.com/repos/kubernetes-sigs/kind/releases/latest | grep '"tag_name":' | cut -d'"' -f4) - echo "version=$KIND_VERSION" >> $GITHUB_OUTPUT - - name: Create k8s kind cluster + go-version-file: go.mod + + - name: Create k8s Kind Cluster uses: helm/kind-action@v1 with: - version: ${{ steps.kind.outputs.version }} - cluster_name: kind - kubectl_version: ${{ steps.kubectl.outputs.version }} - - name: Running E2E Tests + install_only: true + + - name: Running Test e2e run: | go mod tidy make test-e2e diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..7dfc4938 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,26 @@ +name: Tests + +on: + pull_request: + types: [ assigned, opened, synchronize, reopened ] + paths-ignore: + - 'docs/**' + - '**/*.md' + +jobs: + test: + name: Run on Ubuntu + runs-on: ubuntu-latest + steps: + - name: Clone the code + uses: actions/checkout@v5 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Running Tests + run: | + go mod tidy + make test diff --git a/.golangci.yaml b/.golangci.yaml deleted file mode 100644 index 3e2b2b37..00000000 --- a/.golangci.yaml +++ /dev/null @@ -1,197 +0,0 @@ -################################################################################ -# This file is AUTOGENERATED with # -# Edit Makefile.maker.yaml instead. # -################################################################################ - -# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company -# SPDX-License-Identifier: Apache-2.0 - -version: "2" -run: - modules-download-mode: readonly - timeout: 10m0s # none by default in v2 - -formatters: - enable: - - gofmt - - goimports - settings: - goimports: - # Put local imports after 3rd-party packages - local-prefixes: - - github.com/ironcore-dev/network-operator - exclusions: - generated: lax - paths: - - third_party$ - - builtin$ - - examples$ - -issues: - # '0' disables the following options - max-issues-per-linter: 0 - max-same-issues: 0 - -linters: - # Disable all pre-enabled linters and enable them explicitly so that a newer version does not introduce new linters unexpectedly - default: none - enable: - - bodyclose - - containedctx - - copyloopvar - - dupword - - durationcheck - - errcheck - - errname - - errorlint - - exptostd - - forbidigo - - ginkgolinter - - gocheckcompilerdirectives - - goconst - - gocritic - - gomoddirectives - - gosec - - govet - - ineffassign - - intrange - - misspell - - nilerr - - nolintlint - - nosprintfhostport - - perfsprint - - predeclared - - rowserrcheck - - sqlclosecheck - - staticcheck - - unconvert - - unparam - - unused - - usestdlibvars - - usetesting - - whitespace - settings: - errcheck: - check-type-assertions: false - # Report about assignment of errors to blank identifier. - check-blank: true - # Do not report about not checking of errors in type assertions. - # This is not as dangerous as skipping error values because an unchecked type assertion just immediately panics. - # We disable this because it makes a ton of useless noise esp. in test code. - forbidigo: - analyze-types: true # required for pkg: - forbid: - # ioutil package has been deprecated: https://github.com/golang/go/issues/42026 - - pattern: ^ioutil\..*$ - # Using http.DefaultServeMux is discouraged because it's a global variable that some packages silently and magically add handlers to (esp. net/http/pprof). - # Applications wishing to use http.ServeMux should obtain local instances through http.NewServeMux() instead of using the global default instance. - - pattern: ^http\.DefaultServeMux$ - - pattern: ^http\.Handle(?:Func)?$ - - pkg: ^gopkg\.in/square/go-jose\.v2$ - msg: gopk.in/square/go-jose is archived and has CVEs. Replace it with gopkg.in/go-jose/go-jose.v2 - - pkg: ^github.com/coreos/go-oidc$ - msg: github.com/coreos/go-oidc depends on gopkg.in/square/go-jose which has CVEs. Replace it with github.com/coreos/go-oidc/v3 - - pkg: ^github.com/howeyc/gopass$ - msg: github.com/howeyc/gopass is archived, use golang.org/x/term instead - goconst: - min-occurrences: 5 - gocritic: - enabled-checks: - - boolExprSimplify - - builtinShadow - - emptyStringTest - - evalOrder - - httpNoBody - - importShadow - - initClause - - methodExprCall - - paramTypeCombine - - preferFilepathJoin - - ptrToRefParam - - redundantSprint - - returnAfterHttpError - - stringConcatSimplify - - timeExprSimplify - - truncateCmp - - typeAssertChain - - typeUnparen - - unnamedResult - - unnecessaryBlock - - unnecessaryDefer - - weakCond - - yodaStyleExpr - gomoddirectives: - replace-allow-list: - # for go-pmtud - - github.com/mdlayher/arp - # for github.com/sapcc/vpa_butler - - k8s.io/client-go - # until https://github.com/openconfig/ygnmi/pull/157 is merged - - github.com/openconfig/ygnmi - # for CVE-2025-22868 - - golang.org/x/oauth2 - toolchain-forbidden: true - go-version-pattern: 1\.\d+(\.0)?$ - gosec: - excludes: - # gosec wants us to set a short ReadHeaderTimeout to avoid Slowloris attacks, but doing so would expose us to Keep-Alive race conditions (see https://iximiuz.com/en/posts/reverse-proxy-http-keep-alive-and-502s/ - - G112 - # created file permissions are restricted by umask if necessary - - G306 - govet: - disable: - - fieldalignment - enable-all: true - nolintlint: - require-specific: true - staticcheck: - dot-import-whitelist: - - github.com/majewsky/gg/option - - github.com/onsi/ginkgo/v2 - - github.com/onsi/gomega - usestdlibvars: - http-method: true - http-status-code: true - time-weekday: true - time-month: true - time-layout: true - crypto-hash: true - default-rpc-path: true - sql-isolation-level: true - tls-signature-scheme: true - constant-kind: true - usetesting: - os-temp-dir: true - whitespace: - # Enforce newlines (or comments) after multi-line function signatures. - multi-func: true - exclusions: - generated: lax - presets: - - comments - - common-false-positives - - legacy - - std-error-handling - rules: - - linters: - - bodyclose - path: _test\.go - # It is idiomatic Go to reuse the name 'err' with ':=' for subsequent errors. - # Ref: https://go.dev/doc/effective_go#redeclaration - - path: (.+)\.go$ - text: declaration of "err" shadows declaration at - - linters: - - goconst - path: (.+)_test\.go - paths: - - third_party$ - - builtin$ - - examples$ - - internal/provider/openconfig - - internal/provider/cisco/nxos/genyang - -output: - formats: - text: - # Do not print lines of code with issue. - print-issued-lines: false diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000..6f458646 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,145 @@ +version: "2" +run: + allow-parallel-runners: true + timeout: 10m +linters: + default: none + enable: + - bodyclose + - containedctx + - copyloopvar + - dupword + - durationcheck + - errcheck + - errname + - errorlint + - exptostd + - forbidigo + - ginkgolinter + - gocheckcompilerdirectives + - goconst + - gocritic + - gosec + - govet + - ineffassign + - intrange + - misspell + - nilerr + - nolintlint + - nosprintfhostport + - perfsprint + - predeclared + - rowserrcheck + - sqlclosecheck + - staticcheck + - unconvert + - unparam + - unused + - usestdlibvars + - usetesting + - whitespace + settings: + gomoddirectives: + toolchain-forbidden: true + go-version-pattern: 1\.\d+(\.0)?$ + revive: + rules: + - name: comment-spacings + - name: import-shadowing + exclusions: + generated: lax + rules: + - linters: + - perfsprint + - nolintlint + - gosec + - forbidigo + path: test/ + - linters: + - lll + path: api/* + - linters: + - dupl + - lll + path: internal/controller + - linters: + - lll + path: cmd/* + - linters: + - lll + path: test + - linters: + - lll + - gosec + - forbidigo + path: hack/provider + - linters: + - gocyclo + - dupl + path: internal/provider/cisco/nxos/vrf + - linters: + - whitespace + path: internal/provider/cisco/nxos/nve + - linters: + - lll + - goconst + path: internal/provider/cisco/nxos + - linters: + - goimports + - gosec + - perfsprint + - errcheck + path: internal/provider/cisco/nxos/isis + - linters: + - lll + - perfsprint + - gosec + - gocritic + - whitespace + - errorlint + - dupword + path: internal/provider/openconfig + - linters: + - staticcheck + - goconst + - unconvert + - misspell + - gofmt + - gocyclo + - dupl + - gocritic + - whitespace + - perfsprint + - errorlint + path: internal/provider/cisco/nxos/genyang + - linters: + - staticcheck + - goconst + - unused + - unconvert + - gofmt + - errcheck + path: internal/provider/openconfig + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gofmt + - goimports + settings: + goimports: + # Put local imports after 3rd-party packages + local-prefixes: + - github.com/ironcore-dev/network-operator + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ +issues: + max-issues-per-linter: 10 + max-same-issues: 10 + diff --git a/.license-scan-overrides.jsonl b/.license-scan-overrides.jsonl deleted file mode 100644 index 0a8feb2e..00000000 --- a/.license-scan-overrides.jsonl +++ /dev/null @@ -1,11 +0,0 @@ -{"name": "github.com/chzyer/logex", "licenceType": "MIT"} -{"name": "github.com/hashicorp/vault/api/auth/approle", "licenceType": "MPL-2.0"} -{"name": "github.com/jpillora/longestcommon", "licenceType": "MIT"} -{"name": "github.com/logrusorgru/aurora", "licenceType": "Unlicense"} -{"name": "github.com/mattn/go-localereader", "licenceType": "MIT"} -{"name": "github.com/miekg/dns", "licenceType": "BSD-3-Clause"} -{"name": "github.com/pashagolub/pgxmock/v4", "licenceType": "BSD-3-Clause"} -{"name": "github.com/spdx/tools-golang", "licenceTextOverrideFile": "vendor/github.com/spdx/tools-golang/LICENSE.code"} -{"name": "github.com/xeipuuv/gojsonpointer", "licenceType": "Apache-2.0"} -{"name": "github.com/xeipuuv/gojsonreference", "licenceType": "Apache-2.0"} -{"name": "github.com/xeipuuv/gojsonschema", "licenceType": "Apache-2.0"} diff --git a/.license-scan-rules.json b/.license-scan-rules.json deleted file mode 100644 index 909cc0fa..00000000 --- a/.license-scan-rules.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "allowlist": [ - "Apache-2.0", - "BSD-2-Clause", - "BSD-2-Clause-FreeBSD", - "BSD-3-Clause", - "EPL-2.0", - "ISC", - "MIT", - "MPL-2.0", - "Unlicense", - "Zlib" - ] -} diff --git a/Dockerfile b/Dockerfile index b666e722..334ac344 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,43 +2,54 @@ # SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors # SPDX-License-Identifier: Apache-2.0 -FROM golang:1.25-alpine3.22 AS builder - -RUN apk add --no-cache --no-progress git make - -ARG BININFO_BUILD_DATE -ARG BININFO_COMMIT_HASH -ARG BININFO_VERSION +FROM golang:1.25 AS builder ARG TARGETOS ARG TARGETARCH -WORKDIR /workspace - -RUN --mount=type=cache,target=/go/pkg/mod \ - --mount=type=bind,source=go.mod,target=go.mod \ - --mount=type=bind,source=go.sum,target=go.sum \ - go mod download -x - -RUN --mount=type=bind,target=.,readwrite \ - --mount=type=cache,target=/go/pkg/mod \ - --mount=type=cache,target=/root/.cache/go-build \ - GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOTOOLCHAIN=local make install - -FROM gcr.io/distroless/static:nonroot +ARG BUILD_DATE +ARG GIT_COMMIT +ARG VERSION -ARG BININFO_BUILD_DATE -ARG BININFO_COMMIT_HASH -ARG BININFO_VERSION - -LABEL source_repository="https://github.com/ironcore-dev/network-operator" \ +WORKDIR /workspace +# Copy the Go Modules manifests +COPY go.mod go.mod +COPY go.sum go.sum +# cache deps before building and copying source so that we don't need to re-download as much +# and so that source changes don't invalidate our downloaded layer +RUN go mod download + +# Copy the go source +COPY cmd/main.go cmd/main.go +COPY api/ api/ +COPY internal/ internal/ + +# Build +# the GOARCH has not a default value to allow the binary be built according to the host where the command +# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO +# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, +# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. +RUN --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/go/pkg \ + CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -ldflags="-s -w -X 'main.version=${VERSION}' -X 'main.gitCommit=${GIT_COMMIT}' -X 'main.buildDate=${BUILD_DATE}'" -a -o manager cmd/main.go + +# Use distroless as minimal base image to package the manager binary +# Refer to https://github.com/GoogleContainerTools/distroless for more details +FROM gcr.io/distroless/static:nonroot AS manager + +ARG BUILD_DATE +ARG GIT_COMMIT +ARG VERSION + +LABEL source_repository="https://github.com/ironcore-dev/metal-operator" \ org.opencontainers.image.url="https://github.com/ironcore-dev/network-operator" \ - org.opencontainers.image.created=${BININFO_BUILD_DATE} \ - org.opencontainers.image.revision=${BININFO_COMMIT_HASH} \ - org.opencontainers.image.version=${BININFO_VERSION} - -COPY --from=builder /usr/bin/network-operator /manager + org.opencontainers.image.created=${BUILD_DATE} \ + org.opencontainers.image.revision=${GIT_COMMIT} \ + org.opencontainers.image.version=${VERSION} \ + org.opencontainers.image.licenses="Apache-2.0" -USER 65532:65532 WORKDIR / -ENTRYPOINT [ "/manager" ] +COPY --from=builder /workspace/manager . +USER 65532:65532 + +ENTRYPOINT ["/manager"] diff --git a/Makefile b/Makefile index 5a58ea6b..bb725ceb 100644 --- a/Makefile +++ b/Makefile @@ -1,351 +1,343 @@ -################################################################################ -# This file is AUTOGENERATED with # -# Edit Makefile.maker.yaml instead. # -################################################################################ - -# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company -# SPDX-License-Identifier: Apache-2.0 - -MAKEFLAGS=--warn-undefined-variables -# /bin/sh is dash on Debian which does not support all features of ash/bash -# to fix that we use /bin/bash only on Debian to not break Alpine -ifneq (,$(wildcard /etc/os-release)) # check file existence - ifneq ($(shell grep -c debian /etc/os-release),0) - SHELL := /bin/bash - endif -endif -UNAME_S := $(shell uname -s) -SED = sed -XARGS = xargs -ifeq ($(UNAME_S),Darwin) - SED = gsed - XARGS = gxargs -endif - -default: build-all - -# Image to use all building/pushing image targets +# Image URL to use all building/pushing image targets IMG ?= controller:latest +TEST_SERVER_IMG ?= gnmi-test-server:latest -# CONTAINER_TOOL defines the container tool to be used for building images. -# The default is docker, but it can be overridden to use other tools (i.e. podman or nerdctl). -CONTAINER_TOOL ?= docker - -# KIND_CLUSTER_NAME defines the name of the Kind cluster to be used for the tilt setup. -KIND_CLUSTER_NAME ?= network - -install-gofumpt: FORCE - @if ! hash gofumpt 2>/dev/null; then printf "\e[1;36m>> Installing gofumpt...\e[0m\n"; go install mvdan.cc/gofumpt@latest; fi +# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif -install-kustomize: FORCE - @if ! hash kustomize 2>/dev/null; then printf "\e[1;36m>> Installing kustomize...\e[0m\n"; go install sigs.k8s.io/kustomize/kustomize/v5@latest; fi +GOARCH := $(shell go env GOARCH) +GOOS := $(shell go env GOOS) -fmt: FORCE install-gofumpt - @printf "\e[1;36m>> gofumpt -l -w .\e[0m\n" - @gofumpt -l -w $(shell git ls-files '*.go' | grep -v '^internal/provider/openconfig|internal/provider/cisco/nxos/genyang') +VERSION ?= dev +GIT_COMMIT ?= $(shell git rev-parse --short HEAD) +BUILD_DATE ?= $(shell date -u +'%Y-%m-%dT%H:%M:%SZ') -# Run the e2e tests against a k8s cluster. -test-e2e: FORCE - @command -v kind >/dev/null 2>&1 || { \ - echo "Kind is not installed. Please install Kind manually."; \ - exit 1; \ - } - @kind get clusters | grep -q 'kind' || { \ - echo "No Kind cluster is running. Please start a Kind cluster before running the e2e tests."; \ - exit 1; \ - } - @printf "\e[1;36m>> go test ./test/e2e/ -v -ginkgo.v\e[0m\n" - @go test ./test/e2e/ -v -ginkgo.v - -docker-build: FORCE - @printf "\e[1;36m>> $(CONTAINER_TOOL) build --tag=$(IMG) .\e[0m\n" - @$(CONTAINER_TOOL) build --build-arg=BININFO_BUILD_DATE=$(BININFO_BUILD_DATE) --build-arg=BININFO_COMMIT_HASH=$(BININFO_COMMIT_HASH) --build-arg=BININFO_VERSION=$(BININFO_VERSION) --tag=$(IMG) . - -docker-push: FORCE - @printf "\e[1;36m>> $(CONTAINER_TOOL) push $(IMG)\e[0m\n" - @$(CONTAINER_TOOL) push $(IMG) - -# Generate a consolidated YAML with CRDs and deployment. -build-installer: FORCE generate install-kustomize - @printf "\e[1;36m>> kustomize build config/default > dist/install.yaml\e[0m\n" - @cd config/manager && kustomize edit set image controller=$(IMG) - @mkdir -p build; kustomize build config/default > dist/install.yaml - -# Deploy controller to the k8s cluster -deploy: FORCE generate install-kustomize - @printf "\e[1;36m>> kustomize build config/default | kubectl apply -f -\e[0m\n" - @cd config/manager && kustomize edit set image controller=$(IMG) - @kustomize build config/default | kubectl apply -f - - -# Undeploy controller from the k8s cluster -undeploy: FORCE install-kustomize - @printf "\e[1;36m>> kustomize build config/default | kubectl delete -f -\e[0m\n" - @kustomize build config/default | kubectl delete --ignore-not-found=true -f - - -# Install CRDs into the k8s cluster -deploy-crds: FORCE generate install-kustomize - @printf "\e[1;36m>> kustomize build config/crd | kubectl apply -f -\e[0m\n" - @kustomize build config/crd | kubectl apply -f - - -# Uninstall CRDs from the k8s cluster -undeploy-crds: FORCE install-kustomize - @printf "\e[1;36m>> kustomize build config/crd | kubectl delete -f -\e[0m\n" - @kustomize build config/crd | kubectl delete --ignore-not-found=true -f - - -# Create a Kind cluster for local development and testing. -kind-create: FORCE - @command -v kind >/dev/null 2>&1 || { \ - echo "Kind is not installed. Please install Kind manually."; \ - exit 1; \ - } - @kind get clusters | grep -q $(KIND_CLUSTER_NAME) || { \ - printf "\e[1;36m>> kind create cluster --name=$(KIND_CLUSTER_NAME)\e[0m\n"; \ - kind create cluster --name=$(KIND_CLUSTER_NAME); \ - } +LDFLAGS = -ldflags="-X 'main.version=$(VERSION)' -X 'main.gitCommit=$(GIT_COMMIT)' -X 'main.buildDate=$(BUILD_DATE)'" -# Delete the Kind cluster created for local development and testing. -kind-delete: FORCE - @command -v kind >/dev/null 2>&1 || { \ - echo "Kind is not installed. Please install Kind manually."; \ - exit 1; \ - } - @printf "\e[1;36m>> kind delete cluster --name=$(KIND_CLUSTER_NAME)\e[0m\n" - @kind delete cluster --name=$(KIND_CLUSTER_NAME) +# CONTAINER_TOOL defines the container tool to be used for building images. +# Be aware that the target commands are only tested with Docker which is +# scaffolded by default. However, you might want to replace it to use other +# tools. (i.e. podman) +CONTAINER_TOOL ?= docker -tilt-up: FORCE kind-create - @command -v tilt >/dev/null 2>&1 || { \ - echo "Tilt is not installed. Please install Tilt manually."; \ - exit 1; \ +# Setting SHELL to bash allows bash commands to be executed by recipes. +# Options are set to exit when a recipe line exits non-zero or a piped command fails. +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec + +.PHONY: all +all: build + +##@ General + +# The help target prints out all targets with their descriptions organized +# beneath their categories. The categories are represented by '##@' and the +# target descriptions by '##'. The awk command is responsible for reading the +# entire set of makefiles included in this invocation, looking for lines of the +# file as xyz: ## something, and then pretty-format the target and help. Then, +# if there's a line with ##@ something, that gets pretty-printed as a category. +# More info on the usage of ANSI control characters for terminal formatting: +# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters +# More info on the awk command: +# http://linuxcommand.org/lc3_adv_awk.php + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Development + +.PHONY: manifests +manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. + $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + +.PHONY: generate +generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. + $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." + +.PHONY: fmt +fmt: goimports gofumpt ## Run goimports against code. + $(GOIMPORTS) -w . + $(GOFUMPT) -l -w $(shell git ls-files '*.go' | grep -E -v 'internal/provider/openconfig|internal/provider/cisco/nxos/genyang') + +.PHONY: vet +vet: ## Run go vet against code. + go vet ./... + +.PHONY: test +test: manifests generate fmt vet setup-envtest ## Run tests. + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out + +# TODO(user): To use a different vendor for e2e tests, modify the setup under 'tests/e2e'. +# The default setup assumes Kind is pre-installed and builds/loads the Manager Docker image locally. +# CertManager is installed by default; skip with: +# - CERT_MANAGER_INSTALL_SKIP=true +KIND_CLUSTER ?= network-operator-test-e2e + +.PHONY: setup-test-e2e +setup-test-e2e: ## Set up a Kind cluster for e2e tests if it does not exist + @command -v $(KIND) >/dev/null 2>&1 || { \ + echo "Kind is not installed. Please install Kind manually."; \ + exit 1; \ } - @printf "\e[1;36m>> tilt up --context kind-$(KIND_CLUSTER_NAME)\e[0m\n" - @tilt up --context kind-$(KIND_CLUSTER_NAME) + @case "$$($(KIND) get clusters)" in \ + *"$(KIND_CLUSTER)"*) \ + echo "Kind cluster '$(KIND_CLUSTER)' already exists. Skipping creation." ;; \ + *) \ + echo "Creating Kind cluster '$(KIND_CLUSTER)'..."; \ + $(KIND) create cluster --name $(KIND_CLUSTER) ;; \ + esac -# Generate manifests e.g. CRD, RBAC etc. -charts: FORCE generate - @printf "\e[1;36m>> kubebuilder edit --plugins=helm/v1-alpha\e[0m\n" - @kubebuilder edit --plugins=helm/v1-alpha - @rm -rf charts/network-operator && mv dist/chart charts/network-operator && rm -rf dist +.PHONY: test-e2e +test-e2e: setup-test-e2e manifests generate fmt vet ## Run the e2e tests. Expected an isolated environment using Kind. + KIND_CLUSTER=$(KIND_CLUSTER) go test ./test/e2e/ -v -ginkgo.v + $(MAKE) cleanup-test-e2e -netop-provider: - @printf "\e[1;36m>> go build -o build/netop-provider ./hack/provider\e[0m\n" - @go build -o build/netop-provider ./hack/provider - @printf "\e[1;36m>> ./build/netop-provider --help\e[0m\n" - @./build/netop-provider --help +.PHONY: cleanup-test-e2e +cleanup-test-e2e: ## Tear down the Kind cluster used for e2e tests + @$(KIND) delete cluster --name $(KIND_CLUSTER) -TEST_SERVER_IMG ?= ghcr.io/ironcore-dev/gnmi-test-server:latest - -build-test-gnmi-server: FORCE - @printf "\e[1;36m>> $(CONTAINER_TOOL) build --tag=$(TEST_SERVER_IMG) ./test/gnmi\e[0m\n" - @$(CONTAINER_TOOL) build --tag=$(TEST_SERVER_IMG) ./test/gnmi - -run-test-gnmi-server: FORCE build-test-gnmi-server - @printf "\e[1;36m>> $(CONTAINER_TOOL) run -p 8000:8000 -p 9339:9339 $(TEST_SERVER_IMG)\e[0m\n" - @$(CONTAINER_TOOL) run --rm -p 8000:8000 -p 9339:9339 $(TEST_SERVER_IMG) -install-goimports: FORCE - @if ! hash goimports 2>/dev/null; then printf "\e[1;36m>> Installing goimports (this may take a while)...\e[0m\n"; go install golang.org/x/tools/cmd/goimports@latest; fi +.PHONY: lint +lint: golangci-lint ## Run golangci-lint linter + $(GOLANGCI_LINT) run -install-golangci-lint: FORCE - @if ! hash golangci-lint 2>/dev/null; then printf "\e[1;36m>> Installing golangci-lint (this may take a while)...\e[0m\n"; go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest; fi +.PHONY: lint-fix +lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes + $(GOLANGCI_LINT) run --fix -install-modernize: FORCE - @if ! hash modernize 2>/dev/null; then printf "\e[1;36m>> Installing modernize (this may take a while)...\e[0m\n"; go install golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest; fi +.PHONY: lint-config +lint-config: golangci-lint ## Verify golangci-lint linter configuration + $(GOLANGCI_LINT) config verify -install-shellcheck: FORCE - @if ! hash shellcheck 2>/dev/null; then printf "\e[1;36m>> Installing shellcheck...\e[0m\n"; SHELLCHECK_ARCH=$(shell uname -m); SHELLCHECK_OS=$(shell uname -s | tr '[:upper:]' '[:lower:]'); if [[ "$$SHELLCHECK_OS" == "darwin" ]]; then SHELLCHECK_OS=macos; fi; SHELLCHECK_VERSION="stable"; if command -v curl >/dev/null 2>&1; then GET="curl -sLo-"; elif command -v wget >/dev/null 2>&1; then GET="wget -O-"; else echo "Didn't find curl or wget to download shellcheck"; exit 2; fi; $$GET "https://github.com/koalaman/shellcheck/releases/download/$$SHELLCHECK_VERSION/shellcheck-$$SHELLCHECK_VERSION.$$SHELLCHECK_OS.$$SHELLCHECK_ARCH.tar.xz" | tar -Jxf -; BIN=$$(go env GOBIN); if [[ -z $$BIN ]]; then BIN=$$(go env GOPATH)/bin; fi; install -Dm755 shellcheck-$$SHELLCHECK_VERSION/shellcheck -t "$$BIN"; rm -rf shellcheck-$$SHELLCHECK_VERSION; fi +.PHONY: add-license +add-license: addlicense ## Add license headers to all go files. + find . -name '*.go' -exec $(ADDLICENSE) -f hack/license-header.txt {} + -install-go-licence-detector: FORCE - @if ! hash go-licence-detector 2>/dev/null; then printf "\e[1;36m>> Installing go-licence-detector (this may take a while)...\e[0m\n"; go install go.elastic.co/go-licence-detector@latest; fi +.PHONY: check-license +check-license: addlicense ## Check that every file has a license header present. + find . -name '*.go' -exec $(ADDLICENSE) -check -c 'IronCore contributors' {} + -install-addlicense: FORCE - @if ! hash addlicense 2>/dev/null; then printf "\e[1;36m>> Installing addlicense (this may take a while)...\e[0m\n"; go install github.com/google/addlicense@latest; fi +.PHONY: check +check: generate manifests add-license fmt lint test # Generate manifests, code, lint, add licenses, test -install-reuse: FORCE - @if ! hash reuse 2>/dev/null; then if ! hash pip3 2>/dev/null; then printf "\e[1;31m>> Cannot install reuse because no pip3 was found. Either install it using your package manager or install pip3\e[0m\n"; else printf "\e[1;36m>> Installing reuse...\e[0m\n"; pip3 install --user reuse; fi; fi +##@ Build -prepare-static-check: FORCE install-golangci-lint install-modernize install-shellcheck install-go-licence-detector install-addlicense install-reuse +.PHONY: docs +docs: gen-crd-api-reference-docs ## Run go generate to generate API reference documentation. + $(GEN_CRD_API_REFERENCE_DOCS) -api-dir ./api/v1alpha1 -config ./hack/api-reference/config.json -template-dir ./hack/api-reference/template -out-file ./docs/api-reference/api.md -install-controller-gen: FORCE - @if ! hash controller-gen 2>/dev/null; then printf "\e[1;36m>> Installing controller-gen (this may take a while)...\e[0m\n"; go install sigs.k8s.io/controller-tools/cmd/controller-gen@latest; fi +.PHONY: build +build: manifests generate fmt vet ## Build manager binary. + go build $(LDFLAGS) -o bin/manager cmd/main.go -install-setup-envtest: FORCE - @if ! hash setup-envtest 2>/dev/null; then printf "\e[1;36m>> Installing setup-envtest (this may take a while)...\e[0m\n"; go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest; fi +.PHONY: run +run: manifests generate fmt vet ## Run a controller from your host. + go run ./cmd/manager/main.go -GO_BUILDFLAGS = -GO_LDFLAGS = -GO_TESTENV = -GO_BUILDENV = CGO_ENABLED=0 - -# These definitions are overridable, e.g. to provide fixed version/commit values when -# no .git directory is present or to provide a fixed build date for reproducibility. -BININFO_VERSION ?= $(shell git describe --tags --always --abbrev=7) -BININFO_COMMIT_HASH ?= $(shell git rev-parse --verify HEAD) -BININFO_BUILD_DATE ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ") +.PHONY: helm +helm: manifests kubebuilder + $(KUBEBUILDER) edit --plugins=helm/v1-alpha + @rm -rf charts/network-operator && mv dist/chart charts/network-operator && rm -rf dist -build-all: build/network-operator +# If you wish to build the manager image targeting other platforms you can use the --platform flag. +# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it. +# More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +.PHONY: docker-build +docker-build: ## Build docker image with the manager. + $(CONTAINER_TOOL) build \ + --build-arg=BUILD_DATE=$(BUILD_DATE) \ + --build-arg=GIT_COMMIT=$(GIT_COMMIT) \ + --build-arg=VERSION=$(VERSION) \ + -t ${IMG} . + +.PHONY: docker-push +docker-push: ## Push docker image with the manager. + $(CONTAINER_TOOL) push ${IMG} + +# PLATFORMS defines the target platforms for the manager image be built to provide support to multiple +# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to: +# - be able to use docker buildx. More info: https://docs.docker.com/build/buildx/ +# - have enabled BuildKit. More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +# - be able to push the image to your registry (i.e. if you do not set a valid value via IMG=> then the export will fail) +# To adequately provide solutions that are compatible with multiple platforms, you should consider using this option. +PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le +.PHONY: docker-buildx +docker-buildx: ## Build and push docker image for the manager for cross-platform support + # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile + sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross + - $(CONTAINER_TOOL) buildx create --name foo-builder + $(CONTAINER_TOOL) buildx use foo-builder + - $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . + - $(CONTAINER_TOOL) buildx rm foo-builder + rm Dockerfile.crosss + +.PHONY: build-installer +build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment. + mkdir -p dist + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build config/default > dist/install.yaml + +.PHONY: build-test-gnmi-server +build-test-gnmi-server: ## Build docker image with the gnmi test server. + $(CONTAINER_TOOL) build -t ${TEST_SERVER_IMG} ./test/gnmi/ + +run-test-gnmi-server: build-test-gnmi-server + @$(CONTAINER_TOOL) run --rm -p 8000:8000 -p 9339:9339 $(TEST_SERVER_IMG) -build/network-operator: FORCE generate - env $(GO_BUILDENV) go build $(GO_BUILDFLAGS) -ldflags '-s -w -X github.com/sapcc/go-api-declarations/bininfo.binName=network-operator -X github.com/sapcc/go-api-declarations/bininfo.version=$(BININFO_VERSION) -X github.com/sapcc/go-api-declarations/bininfo.commit=$(BININFO_COMMIT_HASH) -X github.com/sapcc/go-api-declarations/bininfo.buildDate=$(BININFO_BUILD_DATE) $(GO_LDFLAGS)' -o build/network-operator ./cmd +##@ Deployment -DESTDIR = -ifeq ($(shell uname -s),Darwin) - PREFIX = /usr/local -else - PREFIX = /usr +ifndef ignore-not-found + ignore-not-found = false endif -install: FORCE build/network-operator - install -d -m 0755 "$(DESTDIR)$(PREFIX)/bin" - install -m 0755 build/network-operator "$(DESTDIR)$(PREFIX)/bin/network-operator" +.PHONY: install +install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. + $(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f - + +.PHONY: uninstall +uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/crd | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - + +.PHONY: deploy +deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build config/default | $(KUBECTL) apply -f - + +.PHONY: undeploy +undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - + +##@ Dependencies + +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +CURL_RETRIES=3 + +## Tool Binaries +KUBECTL ?= kubectl +KIND ?= kind +KUSTOMIZE ?= $(LOCALBIN)/kustomize +CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen +ENVTEST ?= $(LOCALBIN)/setup-envtest +ADDLICENSE ?= $(LOCALBIN)/addlicense +GEN_CRD_API_REFERENCE_DOCS ?= $(LOCALBIN)/gen-crd-api-reference-docs +KUBEBUILDER ?= $(LOCALBIN)/kubebuilder +GOLANGCI_LINT = $(LOCALBIN)/golangci-lint +GOIMPORTS ?= $(LOCALBIN)/goimports +GOFUMPT ?= $(LOCALBIN)/gofumpt +NETOP_PROVIDER ?= $(LOCALBIN)/netop-provider + +## Tool Versions +KUSTOMIZE_VERSION ?= v5.6.0 +CONTROLLER_TOOLS_VERSION ?= v0.18.0 +#ENVTEST_VERSION is the version of controller-runtime release branch to fetch the envtest setup script (i.e. release-0.20) +ENVTEST_VERSION ?= $(shell go list -m -f "{{ .Version }}" sigs.k8s.io/controller-runtime | awk -F'[v.]' '{printf "release-%d.%d", $$2, $$3}') +#ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31) +ENVTEST_K8S_VERSION ?= $(shell go list -m -f "{{ .Version }}" k8s.io/api | awk -F'[v.]' '{printf "1.%d.%d",$$3, $$2}') +GOLANGCI_LINT_VERSION ?= v2.5 +GOIMPORTS_VERSION ?= v0.31.0 +GEN_CRD_API_REFERENCE_DOCS_VERSION ?= v0.3.0 +KUBEBUILDER_VERSION ?= v4.5.1 +ADDLICENSE_VERSION ?= v1.1.1 +GOFUMPT_VERSION ?= v0.8.0 + +.PHONY: addlicense +addlicense: $(ADDLICENSE) ## Download addlicense locally if necessary. +$(ADDLICENSE): $(LOCALBIN) + $(call go-install-tool,$(ADDLICENSE),github.com/google/addlicense,$(ADDLICENSE_VERSION)) + +.PHONY: kustomize +kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. +$(KUSTOMIZE): $(LOCALBIN) + $(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION)) + +.PHONY: controller-gen +controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. +$(CONTROLLER_GEN): $(LOCALBIN) + $(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen,$(CONTROLLER_TOOLS_VERSION)) + +.PHONY: setup-envtest +setup-envtest: envtest ## Download the binaries required for ENVTEST in the local bin directory. + @echo "Setting up envtest binaries for Kubernetes version $(ENVTEST_K8S_VERSION)..." + @$(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path || { \ + echo "Error: Failed to set up envtest binaries for version $(ENVTEST_K8S_VERSION)."; \ + exit 1; \ + } -# which packages to test with test runner -GO_TESTPKGS := $(shell go list -f '{{if or .TestGoFiles .XTestGoFiles}}{{.Dir}}{{end}}' ./... | grep -Ev '/test/e2e') -ifeq ($(GO_TESTPKGS),) -GO_TESTPKGS := ./... -endif -# which packages to measure coverage for -GO_COVERPKGS := $(shell go list ./... | grep -E '/internal' | grep -Ev '/internal/provider/openconfig|internal/provider/cisco/nxos/genyang') -# to get around weird Makefile syntax restrictions, we need variables containing nothing, a space and comma -null := -space := $(null) $(null) -comma := , - -check: FORCE static-check build/cover.html build-all - @printf "\e[1;32m>> All checks successful.\e[0m\n" - -generate: install-controller-gen - @printf "\e[1;36m>> controller-gen\e[0m\n" - @controller-gen crd rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases - @controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..." - -run-golangci-lint: FORCE install-golangci-lint - @printf "\e[1;36m>> golangci-lint\e[0m\n" - @golangci-lint config verify - @golangci-lint run - -run-modernize: FORCE install-modernize - @printf "\e[1;36m>> modernize\e[0m\n" - @modernize $(GO_TESTPKGS) - -run-shellcheck: FORCE install-shellcheck - @printf "\e[1;36m>> shellcheck\e[0m\n" - @find . -type f \( -name '*.bash' -o -name '*.ksh' -o -name '*.zsh' -o -name '*.sh' -o -name '*.shlib' \) -exec shellcheck {} + - -build/cover.out: FORCE generate install-setup-envtest | build - @printf "\e[1;36m>> Running tests\e[0m\n" - KUBEBUILDER_ASSETS=$$(setup-envtest use 1.32 -p path) go run github.com/onsi/ginkgo/v2/ginkgo run --randomize-all -output-dir=build $(GO_BUILDFLAGS) -ldflags '-s -w -X github.com/sapcc/go-api-declarations/bininfo.binName=network-operator -X github.com/sapcc/go-api-declarations/bininfo.version=$(BININFO_VERSION) -X github.com/sapcc/go-api-declarations/bininfo.commit=$(BININFO_COMMIT_HASH) -X github.com/sapcc/go-api-declarations/bininfo.buildDate=$(BININFO_BUILD_DATE) $(GO_LDFLAGS)' -covermode=count -coverpkg=$(subst $(space),$(comma),$(GO_COVERPKGS)) $(GO_TESTPKGS) - @mv build/coverprofile.out build/cover.out - -build/cover.html: build/cover.out - @printf "\e[1;36m>> go tool cover > build/cover.html\e[0m\n" - @go tool cover -html $< -o $@ - -check-addlicense: FORCE install-addlicense - @printf "\e[1;36m>> addlicense --check\e[0m\n" - @addlicense --check -- $(patsubst $(shell awk '$$1 == "module" {print $$2}' go.mod)%,.%/*.go,$(shell go list ./...)) - -check-reuse: FORCE install-reuse - @printf "\e[1;36m>> reuse lint\e[0m\n" - @if ! reuse lint -q; then reuse lint; fi - -check-license-headers: FORCE check-addlicense check-reuse - -__static-check: FORCE run-shellcheck run-golangci-lint run-modernize check-dependency-licenses check-license-headers - -static-check: FORCE - @$(MAKE) --keep-going --no-print-directory __static-check - -build: - @mkdir $@ - -tidy-deps: FORCE - go mod tidy - go mod verify - -license-headers: FORCE install-addlicense install-reuse - @printf "\e[1;36m>> addlicense (for license headers on source code files)\e[0m\n" - @printf "%s\0" $(patsubst $(shell awk '$$1 == "module" {print $$2}' go.mod)%,.%/*.go,$(shell go list ./...)) | $(XARGS) -0 -I{} bash -c 'year="$$(grep 'Copyright' {} | head -n1 | grep -E -o '"'"'[0-9]{4}(-[0-9]{4})?'"'"')"; if [[ -z "$$year" ]]; then year=$$(date +%Y); fi; gawk -i inplace '"'"'{if (display) {print} else {!/^\/\*/ && !/^\*/}}; {if (!display && $$0 ~ /^(package |$$)/) {display=1} else { }}'"'"' {}; addlicense -c "SAP SE or an SAP affiliate company" -s=only -y "$$year" -- {}; $(SED) -i '"'"'1s+// Copyright +// SPDX-FileCopyrightText: +'"'"' {}; ' - @printf "\e[1;36m>> reuse annotate (for license headers on other files)\e[0m\n" - @reuse lint -j | jq -r '.non_compliant.missing_licensing_info[]' | grep -vw vendor | $(XARGS) reuse annotate -c 'SAP SE or an SAP affiliate company' -l Apache-2.0 --skip-unrecognised - @printf "\e[1;36m>> reuse download --all\e[0m\n" - @reuse download --all - @printf "\e[1;35mPlease review the changes. If *.license files were generated, consider instructing go-makefile-maker to add overrides to REUSE.toml instead.\e[0m\n" - -check-dependency-licenses: FORCE install-go-licence-detector - @printf "\e[1;36m>> go-licence-detector\e[0m\n" - @go list -m -mod=readonly -json all | go-licence-detector -includeIndirect -rules .license-scan-rules.json -overrides .license-scan-overrides.jsonl - -goimports: FORCE install-goimports - @printf "\e[1;36m>> goimports -w -local https://github.com/ironcore-dev/network-operator\e[0m\n" - @goimports -w -local github.com/ironcore-dev/network-operator $(patsubst $(shell awk '$$1 == "module" {print $$2}' go.mod)%,.%/*.go,$(shell go list ./...)) - -modernize: FORCE install-modernize - @printf "\e[1;36m>> modernize -fix ./...\e[0m\n" - @modernize -fix ./... - -clean: FORCE - git clean -dxf build - -vars: FORCE - @printf "BININFO_BUILD_DATE=$(BININFO_BUILD_DATE)\n" - @printf "BININFO_COMMIT_HASH=$(BININFO_COMMIT_HASH)\n" - @printf "BININFO_VERSION=$(BININFO_VERSION)\n" - @printf "DESTDIR=$(DESTDIR)\n" - @printf "GO_BUILDENV=$(GO_BUILDENV)\n" - @printf "GO_BUILDFLAGS=$(GO_BUILDFLAGS)\n" - @printf "GO_COVERPKGS=$(GO_COVERPKGS)\n" - @printf "GO_LDFLAGS=$(GO_LDFLAGS)\n" - @printf "GO_TESTPKGS=$(GO_TESTPKGS)\n" - @printf "MAKE=$(MAKE)\n" - @printf "PREFIX=$(PREFIX)\n" - @printf "SED=$(SED)\n" - @printf "UNAME_S=$(UNAME_S)\n" - @printf "XARGS=$(XARGS)\n" -help: FORCE - @printf "\n" - @printf "\e[1mUsage:\e[0m\n" - @printf " make \e[36m\e[0m\n" - @printf "\n" - @printf "\e[1mGeneral\e[0m\n" - @printf " \e[36mvars\e[0m Display values of relevant Makefile variables.\n" - @printf " \e[36mhelp\e[0m Display this help.\n" - @printf "\n" - @printf "\e[1mPrepare\e[0m\n" - @printf " \e[36minstall-goimports\e[0m Install goimports required by goimports/static-check\n" - @printf " \e[36minstall-golangci-lint\e[0m Install golangci-lint required by run-golangci-lint/static-check\n" - @printf " \e[36minstall-modernize\e[0m Install modernize required by run-modernize/static-check\n" - @printf " \e[36minstall-shellcheck\e[0m Install shellcheck required by run-shellcheck/static-check\n" - @printf " \e[36minstall-go-licence-detector\e[0m Install-go-licence-detector required by check-dependency-licenses/static-check\n" - @printf " \e[36minstall-addlicense\e[0m Install addlicense required by check-license-headers/license-headers/static-check\n" - @printf " \e[36minstall-reuse\e[0m Install reuse required by license-headers/check-reuse\n" - @printf " \e[36mprepare-static-check\e[0m Install any tools required by static-check. This is used in CI before dropping privileges, you should probably install all the tools using your package manager\n" - @printf " \e[36minstall-controller-gen\e[0m Install controller-gen required by static-check and build-all. This is used in CI before dropping privileges, you should probably install all the tools using your package manager\n" - @printf " \e[36minstall-setup-envtest\e[0m Install setup-envtest required by check. This is used in CI before dropping privileges, you should probably install all the tools using your package manager\n" - @printf "\n" - @printf "\e[1mBuild\e[0m\n" - @printf " \e[36mbuild-all\e[0m Build all binaries.\n" - @printf " \e[36mbuild/network-operator\e[0m Build network-operator.\n" - @printf " \e[36minstall\e[0m Install all binaries. This option understands the conventional 'DESTDIR' and 'PREFIX' environment variables for choosing install locations.\n" - @printf "\n" - @printf "\e[1mTest\e[0m\n" - @printf " \e[36mcheck\e[0m Run the test suite (unit tests and golangci-lint).\n" - @printf " \e[36mgenerate\e[0m Generate code for Kubernetes CRDs and deepcopy.\n" - @printf " \e[36mrun-golangci-lint\e[0m Install and run golangci-lint. Installing is used in CI, but you should probably install golangci-lint using your package manager.\n" - @printf " \e[36mrun-modernize\e[0m Install and run modernize. Installing is used in CI, but you should probably install modernize using your package manager.\n" - @printf " \e[36mrun-shellcheck\e[0m Install and run shellcheck. Installing is used in CI, but you should probably install shellcheck using your package manager.\n" - @printf " \e[36mbuild/cover.out\e[0m Run tests and generate coverage report.\n" - @printf " \e[36mbuild/cover.html\e[0m Generate an HTML file with source code annotations from the coverage report.\n" - @printf " \e[36mcheck-addlicense\e[0m Check license headers in all non-vendored .go files with addlicense.\n" - @printf " \e[36mcheck-reuse\e[0m Check reuse compliance\n" - @printf " \e[36mcheck-license-headers\e[0m Run static code checks\n" - @printf " \e[36mstatic-check\e[0m Run static code checks\n" - @printf "\n" - @printf "\e[1mDevelopment\e[0m\n" - @printf " \e[36mtidy-deps\e[0m Run go mod tidy and go mod verify.\n" - @printf " \e[36mlicense-headers\e[0m Add (or overwrite) license headers on all non-vendored source code files.\n" - @printf " \e[36mcheck-dependency-licenses\e[0m Check all dependency licenses using go-licence-detector.\n" - @printf " \e[36mgoimports\e[0m Run goimports on all non-vendored .go files\n" - @printf " \e[36mmodernize\e[0m Run modernize on all non-vendored .go files\n" - @printf " \e[36mclean\e[0m Run git clean.\n" - -.PHONY: FORCE +.PHONY: envtest +envtest: $(ENVTEST) ## Download setup-envtest locally if necessary. +$(ENVTEST): $(LOCALBIN) + $(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION)) + +.PHONY: golangci-lint +golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary. +$(GOLANGCI_LINT): $(LOCALBIN) + $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,${GOLANGCI_LINT_VERSION}) + +.PHONY: goimports +goimports: $(GOIMPORTS) ## Download goimports locally if necessary. +$(GOIMPORTS): $(LOCALBIN) + $(call go-install-tool,$(GOIMPORTS),golang.org/x/tools/cmd/goimports,$(GOIMPORTS_VERSION)) + +.PHONY: gen-crd-api-reference-docs +gen-crd-api-reference-docs: $(GEN_CRD_API_REFERENCE_DOCS) ## Download gen-crd-api-reference-docs locally if necessary. +$(GEN_CRD_API_REFERENCE_DOCS): $(LOCALBIN) + $(call go-install-tool,$(GEN_CRD_API_REFERENCE_DOCS),github.com/ahmetb/gen-crd-api-reference-docs,$(GEN_CRD_API_REFERENCE_DOCS_VERSION)) + +.PHONY: kubebuilder +kubebuilder: $(KUBEBUILDER) ## Download kubebuilder locally if necessary. +$(KUBEBUILDER): $(LOCALBIN) + $(call go-install-tool,$(KUBEBUILDER),sigs.k8s.io/kubebuilder/v4,$(KUBEBUILDER_VERSION)) + +.PHONY: netop-provider +netop-provider: $(NETOP_PROVIDER) ## Install the network operator provider binary. +$(NETOP_PROVIDER): $(LOCALBIN) + go build -o $(NETOP_PROVIDER) ./hack/provider/main.go + +.PHONY: gofumpt +gofumpt: $(GOFUMPT) ## Download gofumpt locally if necessary. +$(GOFUMPT): $(LOCALBIN) + $(call go-install-tool,$(GOFUMPT),mvdan.cc/gofumpt,$(GOFUMPT_VERSION)) + +# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist +# $1 - target path with name of binary +# $2 - package url which can be installed +# $3 - specific version of package +define go-install-tool +@[ -f "$(1)-$(3)" ] || { \ +set -e; \ +package=$(2)@$(3) ;\ +echo "Downloading $${package}" ;\ +rm -f $(1) || true ;\ +GOBIN=$(LOCALBIN) go install $${package} ;\ +mv $(1) $(1)-$(3) ;\ +} ;\ +ln -sf $(1)-$(3) $(1) +endef + +## -------------------------------------- +## Tilt / Kind +## -------------------------------------- + +KIND_CLUSTER_NAME ?= network-operator + +.PHONY: kind-create +kind-create: $(ENVTEST) ## create kind cluster if needed + ./hack/kind-with-registry.sh + +.PHONY: kind-delete +kind-delete: ## Destroys the kind cluster. + kind delete cluster --name=$(KIND_CLUSTER_NAME) + docker stop kind-registry && docker rm kind-registry + +.PHONY: tilt-up +tilt-up: $(ENVTEST) $(KUSTOMIZE) kind-create ## start tilt and build kind cluster if needed + EXP_CLUSTER_RESOURCE_SET=true tilt up diff --git a/Makefile.maker.yaml b/Makefile.maker.yaml deleted file mode 100644 index af071d1c..00000000 --- a/Makefile.maker.yaml +++ /dev/null @@ -1,182 +0,0 @@ -# Configuration file for - -metadata: - url: https://github.com/ironcore-dev/network-operator - -binaries: - - name: network-operator - fromPackage: ./cmd - installTo: bin/ - -controllerGen: - enabled: true - crdOutputPath: config/crd/bases - objectHeaderFile: hack/boilerplate.go.txt - rbacRoleName: manager-role - -coverageTest: - only: "/internal" - except: "/internal/provider/openconfig|internal/provider/cisco/nxos/genyang" - -dockerfile: - enabled: false - -golang: - setGoModVersion: true - -golangciLint: - createConfig: true - skipDirs: - - internal/provider/openconfig - - internal/provider/cisco/nxos/genyang - timeout: 10m - -goReleaser: - createConfig: true - -reuse: - enabled: false - -renovate: - enabled: true - assignees: - - felix-kaestner - -testPackages: - except: '/test/e2e' - -githubWorkflow: - global: - defaultBranch: main - ci: - enabled: true - coveralls: false - prepareMakeTarget: generate - license: - enabled: true - release: - enabled: true - securityChecks: - enabled: true - -variables: - GO_BUILDENV: 'CGO_ENABLED=0' - -verbatim: | - # Image to use all building/pushing image targets - IMG ?= controller:latest - - # CONTAINER_TOOL defines the container tool to be used for building images. - # The default is docker, but it can be overridden to use other tools (i.e. podman or nerdctl). - CONTAINER_TOOL ?= docker - - # KIND_CLUSTER_NAME defines the name of the Kind cluster to be used for the tilt setup. - KIND_CLUSTER_NAME ?= network - - install-gofumpt: FORCE - @if ! hash gofumpt 2>/dev/null; then printf "\e[1;36m>> Installing gofumpt...\e[0m\n"; go install mvdan.cc/gofumpt@latest; fi - - install-kustomize: FORCE - @if ! hash kustomize 2>/dev/null; then printf "\e[1;36m>> Installing kustomize...\e[0m\n"; go install sigs.k8s.io/kustomize/kustomize/v5@latest; fi - - fmt: FORCE install-gofumpt - @printf "\e[1;36m>> gofumpt -l -w .\e[0m\n" - @gofumpt -l -w $(shell git ls-files '*.go' | grep -v '^internal/provider/openconfig|internal/provider/cisco/nxos/genyang') - - # Run the e2e tests against a k8s cluster. - test-e2e: FORCE - @command -v kind >/dev/null 2>&1 || { \ - echo "Kind is not installed. Please install Kind manually."; \ - exit 1; \ - } - @kind get clusters | grep -q 'kind' || { \ - echo "No Kind cluster is running. Please start a Kind cluster before running the e2e tests."; \ - exit 1; \ - } - @printf "\e[1;36m>> go test ./test/e2e/ -v -ginkgo.v\e[0m\n" - @go test ./test/e2e/ -v -ginkgo.v - - docker-build: FORCE - @printf "\e[1;36m>> $(CONTAINER_TOOL) build --tag=$(IMG) .\e[0m\n" - @$(CONTAINER_TOOL) build --build-arg=BININFO_BUILD_DATE=$(BININFO_BUILD_DATE) --build-arg=BININFO_COMMIT_HASH=$(BININFO_COMMIT_HASH) --build-arg=BININFO_VERSION=$(BININFO_VERSION) --tag=$(IMG) . - - docker-push: FORCE - @printf "\e[1;36m>> $(CONTAINER_TOOL) push $(IMG)\e[0m\n" - @$(CONTAINER_TOOL) push $(IMG) - - # Generate a consolidated YAML with CRDs and deployment. - build-installer: FORCE generate install-kustomize - @printf "\e[1;36m>> kustomize build config/default > dist/install.yaml\e[0m\n" - @cd config/manager && kustomize edit set image controller=$(IMG) - @mkdir -p build; kustomize build config/default > dist/install.yaml - - # Deploy controller to the k8s cluster - deploy: FORCE generate install-kustomize - @printf "\e[1;36m>> kustomize build config/default | kubectl apply -f -\e[0m\n" - @cd config/manager && kustomize edit set image controller=$(IMG) - @kustomize build config/default | kubectl apply -f - - - # Undeploy controller from the k8s cluster - undeploy: FORCE install-kustomize - @printf "\e[1;36m>> kustomize build config/default | kubectl delete -f -\e[0m\n" - @kustomize build config/default | kubectl delete --ignore-not-found=true -f - - - # Install CRDs into the k8s cluster - deploy-crds: FORCE generate install-kustomize - @printf "\e[1;36m>> kustomize build config/crd | kubectl apply -f -\e[0m\n" - @kustomize build config/crd | kubectl apply -f - - - # Uninstall CRDs from the k8s cluster - undeploy-crds: FORCE install-kustomize - @printf "\e[1;36m>> kustomize build config/crd | kubectl delete -f -\e[0m\n" - @kustomize build config/crd | kubectl delete --ignore-not-found=true -f - - - # Create a Kind cluster for local development and testing. - kind-create: FORCE - @command -v kind >/dev/null 2>&1 || { \ - echo "Kind is not installed. Please install Kind manually."; \ - exit 1; \ - } - @kind get clusters | grep -q $(KIND_CLUSTER_NAME) || { \ - printf "\e[1;36m>> kind create cluster --name=$(KIND_CLUSTER_NAME)\e[0m\n"; \ - kind create cluster --name=$(KIND_CLUSTER_NAME); \ - } - - # Delete the Kind cluster created for local development and testing. - kind-delete: FORCE - @command -v kind >/dev/null 2>&1 || { \ - echo "Kind is not installed. Please install Kind manually."; \ - exit 1; \ - } - @printf "\e[1;36m>> kind delete cluster --name=$(KIND_CLUSTER_NAME)\e[0m\n" - @kind delete cluster --name=$(KIND_CLUSTER_NAME) - - tilt-up: FORCE kind-create - @command -v tilt >/dev/null 2>&1 || { \ - echo "Tilt is not installed. Please install Tilt manually."; \ - exit 1; \ - } - @printf "\e[1;36m>> tilt up --context kind-$(KIND_CLUSTER_NAME)\e[0m\n" - @tilt up --context kind-$(KIND_CLUSTER_NAME) - - # Generate manifests e.g. CRD, RBAC etc. - charts: FORCE generate - @printf "\e[1;36m>> kubebuilder edit --plugins=helm/v1-alpha\e[0m\n" - @kubebuilder edit --plugins=helm/v1-alpha - @rm -rf charts/network-operator && mv dist/chart charts/network-operator && rm -rf dist - - netop-provider: - @printf "\e[1;36m>> go build -o build/netop-provider ./hack/provider\e[0m\n" - @go build -o build/netop-provider ./hack/provider - @printf "\e[1;36m>> ./build/netop-provider --help\e[0m\n" - @./build/netop-provider --help - - TEST_SERVER_IMG ?= ghcr.io/ironcore-dev/gnmi-test-server:latest - - build-test-gnmi-server: FORCE - @printf "\e[1;36m>> $(CONTAINER_TOOL) build --tag=$(TEST_SERVER_IMG) ./test/gnmi\e[0m\n" - @$(CONTAINER_TOOL) build --tag=$(TEST_SERVER_IMG) ./test/gnmi - - run-test-gnmi-server: FORCE build-test-gnmi-server - @printf "\e[1;36m>> $(CONTAINER_TOOL) run -p 8000:8000 -p 9339:9339 $(TEST_SERVER_IMG)\e[0m\n" - @$(CONTAINER_TOOL) run --rm -p 8000:8000 -p 9339:9339 $(TEST_SERVER_IMG) diff --git a/api/v1alpha1/device_types.go b/api/v1alpha1/device_types.go index 0e84f7b3..7e7bda13 100644 --- a/api/v1alpha1/device_types.go +++ b/api/v1alpha1/device_types.go @@ -491,11 +491,11 @@ type DeviceStatus struct { Phase DevicePhase `json:"phase,omitempty"` // The conditions are a list of status objects that describe the state of the Device. - //+listType=map - //+listMapKey=type - //+patchStrategy=merge - //+patchMergeKey=type - //+optional + // +listType=map + // +listMapKey=type + // +patchStrategy=merge + // +patchMergeKey=type + // +optional Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } diff --git a/api/v1alpha1/doc.go b/api/v1alpha1/doc.go new file mode 100644 index 00000000..e3ae75c0 --- /dev/null +++ b/api/v1alpha1/doc.go @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +// Package v1alpha1 contains API Schema definitions for the networking.cloud.sap API group +// +kubebuilder:validation:Required +// +kubebuilder:object:generate=true +// +groupName=networking.cloud.sap +package v1alpha1 diff --git a/api/v1alpha1/groupversion_info.go b/api/v1alpha1/groupversion_info.go index e94846d0..baedbc42 100644 --- a/api/v1alpha1/groupversion_info.go +++ b/api/v1alpha1/groupversion_info.go @@ -2,9 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 // Package v1alpha1 contains API Schema definitions for the networking.cloud.sap v1alpha1 API group. -// +kubebuilder:validation:Required -// +kubebuilder:object:generate=true -// +groupName=networking.cloud.sap package v1alpha1 import ( diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index ae2ddc84..adf2c2a6 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -8,7 +8,7 @@ package v1alpha1 import ( - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) diff --git a/charts/network-operator/Chart.lock b/charts/network-operator/Chart.lock deleted file mode 100644 index d655cac4..00000000 --- a/charts/network-operator/Chart.lock +++ /dev/null @@ -1,6 +0,0 @@ -dependencies: -- name: owner-info - repository: oci://ghcr.io/sapcc/helm-charts - version: 1.0.0 -digest: sha256:7643f231cc4ebda347fd12ec62fe4445c280e2b71d27eec555f3025290f5038f -generated: "2025-08-07T13:01:53.383695+02:00" diff --git a/charts/network-operator/Chart.yaml b/charts/network-operator/Chart.yaml index 2cd7c961..5eb1a44f 100644 --- a/charts/network-operator/Chart.yaml +++ b/charts/network-operator/Chart.yaml @@ -5,8 +5,3 @@ type: application version: 0.1.0 appVersion: "0.1.0" icon: "https://example.com/icon.png" -dependencies: - # See: https://github.com/sapcc/helm-charts/pkgs/container/helm-charts%2Fowner-info - - name: owner-info - repository: oci://ghcr.io/sapcc/helm-charts - version: 1.0.0 diff --git a/charts/network-operator/templates/crd/networking.cloud.sap_devices.yaml b/charts/network-operator/templates/crd/networking.cloud.sap_devices.yaml index 978598d1..d74f4dd9 100755 --- a/charts/network-operator/templates/crd/networking.cloud.sap_devices.yaml +++ b/charts/network-operator/templates/crd/networking.cloud.sap_devices.yaml @@ -21,15 +21,18 @@ spec: scope: Namespaced versions: - additionalPrinterColumns: - - jsonPath: .spec.endpoint + - jsonPath: .spec.endpoint.address name: Endpoint type: string - - jsonPath: .metadata.creationTimestamp - name: Age - type: date - jsonPath: .status.phase name: Phase type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date name: v1alpha1 schema: openAPIV3Schema: @@ -278,10 +281,86 @@ spec: - domain type: object endpoint: - description: Endpoint is the management address of the device provided - as . - pattern: ^(\d{1,3}\.){3}\d{1,3}:\d{1,5}$ - type: string + description: Endpoint contains the connection information for the + device. + properties: + address: + description: Address is the management address of the device provided + as . + pattern: ^(\d{1,3}\.){3}\d{1,3}:\d{1,5}$ + type: string + secretRef: + description: |- + SecretRef is name of the authentication secret for the device containing the username and password. + The secret must be of type kubernetes.io/basic-auth and as such contain the following keys: 'username' and 'password'. + properties: + name: + description: name is unique within a namespace to reference + a secret resource. + type: string + namespace: + description: namespace defines the space within which the + secret name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic + tls: + description: Transport credentials for grpc connection to the + switch. + properties: + ca: + description: The CA certificate to verify the server's identity. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + certificate: + description: |- + The client certificate and private key to use for mutual TLS authentication. + Leave empty if mTLS is not desired. + properties: + secretRef: + description: |- + Secret containing the certificate. + The secret must be of type kubernetes.io/tls and as such contain the following keys: 'tls.crt' and 'tls.key'. + properties: + name: + description: name is unique within a namespace to + reference a secret resource. + type: string + namespace: + description: namespace defines the space within which + the secret name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - secretRef + type: object + required: + - ca + type: object + required: + - address + type: object grpc: description: |- Configuration for the gRPC server on the device. @@ -484,21 +563,6 @@ spec: required: - certificates type: object - secretRef: - description: |- - SecretRef is name of the authentication secret for the device containing the username and password. - The secret must be of type kubernetes.io/basic-auth and as such contain the following keys: 'username' and 'password'. - properties: - name: - description: name is unique within a namespace to reference a - secret resource. - type: string - namespace: - description: namespace defines the space within which the secret - name must be unique. - type: string - type: object - x-kubernetes-map-type: atomic snmp: description: SNMP global configuration. properties: @@ -577,59 +641,6 @@ spec: - location - srcIf type: object - tls: - description: Transport credentials for grpc connection to the switch. - properties: - ca: - description: The CA certificate to verify the server's identity. - properties: - key: - description: The key of the secret to select from. Must be - a valid secret key. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - optional: - description: Specify whether the Secret or its key must be - defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - certificate: - description: |- - The client certificate and private key to use for mutual TLS authentication. - Leave empty if mTLS is not desired. - properties: - secretRef: - description: |- - Secret containing the certificate. - The secret must be of type kubernetes.io/tls and as such contain the following keys: 'tls.crt' and 'tls.key'. - properties: - name: - description: name is unique within a namespace to reference - a secret resource. - type: string - namespace: - description: namespace defines the space within which - the secret name must be unique. - type: string - type: object - x-kubernetes-map-type: atomic - required: - - secretRef - type: object - required: - - ca - type: object users: description: List of local users on the switch. items: @@ -677,7 +688,6 @@ spec: type: object type: array required: - - bootstrap - endpoint type: object status: diff --git a/charts/network-operator/templates/crd/networking.cloud.sap_interfaces.yaml b/charts/network-operator/templates/crd/networking.cloud.sap_interfaces.yaml index fea983a2..8386e211 100755 --- a/charts/network-operator/templates/crd/networking.cloud.sap_interfaces.yaml +++ b/charts/network-operator/templates/crd/networking.cloud.sap_interfaces.yaml @@ -35,6 +35,12 @@ spec: - jsonPath: .spec.mtu name: MTU type: string + - jsonPath: .spec.deviceName + name: Device + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string - jsonPath: .metadata.creationTimestamp name: Age type: date @@ -77,6 +83,25 @@ spec: the interface. maxLength: 255 type: string + deviceRef: + description: |- + DeviceName is the name of the Device this object belongs to. The Device object must exist in the same namespace. + Immutable. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + maxLength: 63 + minLength: 1 + type: string + required: + - name + type: object + x-kubernetes-map-type: atomic + x-kubernetes-validations: + - message: DeviceRef is immutable + rule: self == oldSelf ipv4Addresses: description: |- Ipv4Addresses is the list of IPv4 addresses assigned to the interface. @@ -97,6 +122,40 @@ spec: description: Name is the name of the interface. maxLength: 255 type: string + providerConfigRef: + description: |- + ProviderConfigRef is a reference to a resource holding the provider-specific configuration of this interface. + This reference is used to link the Interface to its provider-specific configuration. + properties: + apiVersion: + description: APIVersion is the api group version of the resource + being referenced. + maxLength: 253 + minLength: 1 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/)?([a-z0-9]([-a-z0-9]*[a-z0-9])?)$ + type: string + kind: + description: |- + Kind of the resource being referenced. + Kind must consist of alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name of the resource being referenced. + Name must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - apiVersion + - kind + - name + type: object + x-kubernetes-map-type: atomic switchport: description: |- Switchport defines the switchport configuration for the interface. @@ -154,6 +213,7 @@ spec: type: string required: - adminState + - deviceRef - name - type type: object @@ -230,6 +290,8 @@ spec: - type x-kubernetes-list-type: map type: object + required: + - spec type: object served: true storage: true diff --git a/charts/network-operator/templates/network-policy/allow-metrics-traffic.yaml b/charts/network-operator/templates/network-policy/allow-metrics-traffic.yaml index eebe1f14..4d381a54 100755 --- a/charts/network-operator/templates/network-policy/allow-metrics-traffic.yaml +++ b/charts/network-operator/templates/network-policy/allow-metrics-traffic.yaml @@ -19,9 +19,9 @@ spec: ingress: # This allows ingress traffic from any namespace with the label metrics: enabled - from: - - namespaceSelector: - matchLabels: - metrics: enabled # Only from namespaces with this label + - namespaceSelector: + matchLabels: + metrics: enabled # Only from namespaces with this label ports: - port: 8443 protocol: TCP diff --git a/charts/network-operator/templates/rbac/role.yaml b/charts/network-operator/templates/rbac/role.yaml index f5b4f58c..d3ef419a 100755 --- a/charts/network-operator/templates/rbac/role.yaml +++ b/charts/network-operator/templates/rbac/role.yaml @@ -11,7 +11,6 @@ rules: - "" resources: - configmaps - - secrets verbs: - get - list @@ -23,6 +22,15 @@ rules: verbs: - create - patch +- apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - update + - watch - apiGroups: - networking.cloud.sap resources: diff --git a/charts/network-operator/values.yaml b/charts/network-operator/values.yaml index 0a7cac5f..9ff6bb35 100644 --- a/charts/network-operator/values.yaml +++ b/charts/network-operator/values.yaml @@ -12,10 +12,10 @@ controllerManager: resources: limits: cpu: 500m - memory: 512Mi + memory: 128Mi requests: - cpu: 50m - memory: 256Mi + cpu: 10m + memory: 64Mi livenessProbe: initialDelaySeconds: 15 periodSeconds: 20 @@ -74,13 +74,3 @@ certmanager: # [NETWORK POLICIES]: To enable NetworkPolicies set true networkPolicy: enable: false - -owner-info: - helm-chart-url: "https://github.com/ironcore-dev/network-operator/charts/network-operator" - maintainers: - - "felix.kaestner@sap.com" - - "andreas.fritzler@sap.com" - - "sebastian.wagner02@sap.com" - - "enric.pujol@sap.com" - support-group: "network-api" - enabled: true diff --git a/cmd/main.go b/cmd/main.go index fd87ae91..72cf4d26 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -7,27 +7,23 @@ import ( "crypto/tls" "flag" "fmt" + "log" "os" "path/filepath" "strings" - // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) - // to ensure that exec-entrypoint and run can make use of them. - _ "k8s.io/client-go/plugin/pkg/client/auth" - "k8s.io/utils/ptr" - - // Set runtime concurrency to match CPU limit imposed by Kubernetes - _ "go.uber.org/automaxprocs" - - "github.com/sapcc/go-api-declarations/bininfo" "go.uber.org/zap/zapcore" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" - ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/cache" + + // Set runtime concurrency to match CPU limit imposed by Kubernetes + _ "go.uber.org/automaxprocs" + + _ "k8s.io/client-go/plugin/pkg/client/auth" + ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/certwatcher" - "sigs.k8s.io/controller-runtime/pkg/config" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" "sigs.k8s.io/controller-runtime/pkg/metrics/filters" @@ -47,6 +43,10 @@ import ( var ( scheme = runtime.NewScheme() setupLog = ctrl.Log.WithName("setup") + + version = "dev" + gitCommit = "none" + buildDate = "unknown" ) func init() { @@ -56,8 +56,13 @@ func init() { } func main() { - // if called with `--version`, report version and exit - bininfo.HandleVersionArgument() + if len(os.Args) > 1 && os.Args[1] == "version" { + log.SetFlags(0) + log.Printf("Version: %s", version) + log.Printf("Git Commit: %s", gitCommit) + log.Printf("Build Date: %s", buildDate) + os.Exit(0) + } var metricsAddr string var metricsCertPath, metricsCertName, metricsCertKey string @@ -82,6 +87,7 @@ func main() { flag.BoolVar(&enableHTTP2, "enable-http2", false, "If set, HTTP/2 will be enabled for the metrics and webhook servers") flag.StringVar(&watchFilterValue, "watch-filter", "", fmt.Sprintf("Label value that the controller watches to reconcile api objects. Label key is always %q. If unspecified, the controller watches for all api objects.", v1alpha1.WatchLabel)) flag.StringVar(&providerName, "provider", "openconfig", "The provider to use for the controller. If not specified, the default provider is used. Available providers: "+strings.Join(provider.Providers(), ", ")) + opts := zap.Options{ Development: true, TimeEncoder: zapcore.ISO8601TimeEncoder, @@ -180,7 +186,6 @@ func main() { mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Cache: cache.Options{ReaderFailOnMissingInformer: true}, - Controller: config.Controller{UsePriorityQueue: ptr.To(true)}, Scheme: scheme, Metrics: metricsServerOptions, WebhookServer: webhookServer, diff --git a/config/crd/bases/networking.cloud.sap_interfaces.yaml b/config/crd/bases/networking.cloud.sap_interfaces.yaml index 5342264b..e4e9efb1 100644 --- a/config/crd/bases/networking.cloud.sap_interfaces.yaml +++ b/config/crd/bases/networking.cloud.sap_interfaces.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.19.0 + controller-gen.kubebuilder.io/version: v0.18.0 name: interfaces.networking.cloud.sap spec: group: networking.cloud.sap diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index ce228eac..37d2b63f 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -58,41 +58,42 @@ spec: seccompProfile: type: RuntimeDefault containers: - - command: - - /manager - args: - - --leader-elect - - --health-probe-bind-address=:8081 - image: controller:latest - imagePullPolicy: IfNotPresent - name: manager - ports: [] - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - "ALL" - livenessProbe: - httpGet: - path: /healthz - port: 8081 - initialDelaySeconds: 15 - periodSeconds: 20 - readinessProbe: - httpGet: - path: /readyz - port: 8081 - initialDelaySeconds: 5 - periodSeconds: 10 - # TODO(user): Configure the resources accordingly based on the project requirements. - # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - resources: - requests: - cpu: 50m - memory: 256Mi - limits: - memory: 512Mi - volumeMounts: [] + - command: + - /manager + args: + - --leader-elect + - --health-probe-bind-address=:8081 + image: controller:latest + name: manager + ports: [] + securityContext: + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + # TODO(user): Configure the resources accordingly based on the project requirements. + # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + resources: + limits: + cpu: 1000m + memory: 512Mi + requests: + cpu: 10m + memory: 64Mi + volumeMounts: [] volumes: [] serviceAccountName: controller-manager terminationGracePeriodSeconds: 10 diff --git a/config/network-policy/allow-metrics-traffic.yaml b/config/network-policy/allow-metrics-traffic.yaml index 7defccca..39e122bf 100644 --- a/config/network-policy/allow-metrics-traffic.yaml +++ b/config/network-policy/allow-metrics-traffic.yaml @@ -19,9 +19,9 @@ spec: ingress: # This allows ingress traffic from any namespace with the label metrics: enabled - from: - - namespaceSelector: - matchLabels: - metrics: enabled # Only from namespaces with this label + - namespaceSelector: + matchLabels: + metrics: enabled # Only from namespaces with this label ports: - port: 8443 protocol: TCP diff --git a/docs/api-reference/api.md b/docs/api-reference/api.md new file mode 100644 index 00000000..80fb4c90 --- /dev/null +++ b/docs/api-reference/api.md @@ -0,0 +1,2310 @@ +

Packages:

+ +

networking.cloud.sap/v1alpha1

+
+

Package v1alpha1 contains API Schema definitions for the networking.cloud.sap API group

+
+Resource Types: +
    +

    ACL +

    +
    +
    + + + + + + + + + + + + + + + + + +
    FieldDescription
    +name
    + +string + +
    +

    The name of the access control list.

    +
    +entries
    + + +[]*./api/v1alpha1.ACLEntry + + +
    +

    A list of rules/entries to apply.

    +
    +

    ACLAction +(string alias)

    +

    +(Appears on:ACLEntry) +

    +
    +

    ACLAction represents the type of action that can be taken by an ACL rule.

    +
    + + + + + + + + + + + + +
    ValueDescription

    "Deny"

    ActionDeny blocks traffic that matches the rule.

    +

    "Permit"

    ActionPermit allows traffic that matches the rule.

    +
    +

    ACLEntry +

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +sequence
    + +int + +
    +

    The sequence number of the ACL entry.

    +
    +action
    + + +ACLAction + + +
    +

    The forwarding action of the ACL entry.

    +
    +protocol
    + +string + +
    +(Optional) +

    The protocol to match. If not specified, defaults to “ip” (IPv4).

    +
    +sourceAddress
    + + +IPPrefix + + +
    +

    Source IPv4 address prefix. Use 0.0.0.0/0 to represent ‘any’.

    +
    +destinationAddress
    + + +IPPrefix + + +
    +

    Destination IPv4 address prefix. Use 0.0.0.0/0 to represent ‘any’.

    +
    +

    AdminState +(string alias)

    +

    +(Appears on:InterfaceSpec) +

    +
    +

    AdminState represents the administrative state of the interface.

    +
    + + + + + + + + + + + + +
    ValueDescription

    "Down"

    AdminStateDown indicates that the interface is administratively set down.

    +

    "Up"

    AdminStateUp indicates that the interface is administratively set up.

    +
    +

    Bootstrap +

    +

    +(Appears on:DeviceSpec) +

    +
    +

    Bootstrap defines the configuration for device bootstrap.

    +
    + + + + + + + + + + + + + +
    FieldDescription
    +template
    + + +TemplateSource + + +
    +

    Template defines the multiline string template that contains the initial configuration for the device.

    +
    +

    Certificate +

    +
    +
    + + + + + + + + + + + + + + + + + +
    FieldDescription
    +name
    + +string + +
    +

    The name of the certificate.

    +
    +source
    + + +CertificateSource + + +
    +

    The source of the certificate content.

    +
    +

    CertificateSource +

    +

    +(Appears on:Certificate, TLS) +

    +
    +

    CertificateSource represents a source for the value of a certificate.

    +
    + + + + + + + + + + + + + +
    FieldDescription
    +secretRef
    + + +Kubernetes core/v1.SecretReference + + +
    +

    Secret containing the certificate. +The secret must be of type kubernetes.io/tls and as such contain the following keys: ‘tls.crt’ and ‘tls.key’.

    +
    +

    DNS +

    +

    +(Appears on:DeviceSpec) +

    +
    +
    + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +domain
    + +string + +
    +

    Default domain name that the switch uses to complete unqualified hostnames.

    +
    +servers
    + + +[]*./api/v1alpha1.NameServer + + +
    +(Optional) +

    A list of DNS servers to use for address resolution.

    +
    +srcIf
    + +string + +
    +(Optional) +

    Source interface for all DNS traffic.

    +
    +

    Device +

    +
    +

    Device is the Schema for the devices API.

    +
    + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +metadata
    + + +Kubernetes meta/v1.ObjectMeta + + +
    +Refer to the Kubernetes API documentation for the fields of the +metadata field. +
    +spec
    + + +DeviceSpec + + +
    +

    Specification of the desired state of the resource. +More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +endpoint
    + + +Endpoint + + +
    +

    Endpoint contains the connection information for the device.

    +
    +bootstrap
    + + +Bootstrap + + +
    +(Optional) +

    Bootstrap is an optional configuration for the device bootstrap process. +It can be used to provide initial configuration templates or scripts that are applied during the device provisioning.

    +
    +dns
    + + +DNS + + +
    +(Optional) +

    Top-level configuration for DNS / resolver.

    +
    +ntp
    + + +NTP + + +
    +(Optional) +

    Configuration data for system-wide NTP process.

    +
    +acl
    + + +[]*./api/v1alpha1.ACL + + +
    +(Optional) +

    Access Control Lists (ACLs) configuration.

    +
    +pki
    + + +PKI + + +
    +(Optional) +

    PKI configuration for managing certificates on the device.

    +
    +logging
    + + +Logging + + +
    +(Optional) +

    Top-level logging configuration for the device.

    +
    +snmp
    + + +SNMP + + +
    +(Optional) +

    SNMP global configuration.

    +
    +users
    + + +[]*./api/v1alpha1.User + + +
    +(Optional) +

    List of local users on the switch.

    +
    +grpc
    + + +GRPC + + +
    +(Optional) +

    Configuration for the gRPC server on the device. +Currently, only a single “default” gRPC server is supported.

    +
    +banner
    + + +TemplateSource + + +
    +(Optional) +

    MOTD banner to display on login.

    +
    +
    +status
    + + +DeviceStatus + + +
    +

    Status of the resource. This is set and updated automatically. +Read-only. +More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status

    +
    +

    DevicePhase +(string alias)

    +

    +(Appears on:DeviceStatus) +

    +
    +

    DevicePhase represents the current phase of the Device as it’s being provisioned and managed by the operator.

    +
    + + + + + + + + + + + + + + + + +
    ValueDescription

    "Active"

    DevicePhaseActive indicates that the device has been successfully provisioned and is now ready for use.

    +

    "Failed"

    DevicePhaseFailed indicates that the device provisioning has failed.

    +

    "Pending"

    DevicePhasePending indicates that the device is pending and has not yet been provisioned.

    +

    "Provisioning"

    DevicePhaseProvisioning indicates that the device is being provisioned.

    +
    +

    DeviceSpec +

    +

    +(Appears on:Device) +

    +
    +

    DeviceSpec defines the desired state of Device.

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +endpoint
    + + +Endpoint + + +
    +

    Endpoint contains the connection information for the device.

    +
    +bootstrap
    + + +Bootstrap + + +
    +(Optional) +

    Bootstrap is an optional configuration for the device bootstrap process. +It can be used to provide initial configuration templates or scripts that are applied during the device provisioning.

    +
    +dns
    + + +DNS + + +
    +(Optional) +

    Top-level configuration for DNS / resolver.

    +
    +ntp
    + + +NTP + + +
    +(Optional) +

    Configuration data for system-wide NTP process.

    +
    +acl
    + + +[]*./api/v1alpha1.ACL + + +
    +(Optional) +

    Access Control Lists (ACLs) configuration.

    +
    +pki
    + + +PKI + + +
    +(Optional) +

    PKI configuration for managing certificates on the device.

    +
    +logging
    + + +Logging + + +
    +(Optional) +

    Top-level logging configuration for the device.

    +
    +snmp
    + + +SNMP + + +
    +(Optional) +

    SNMP global configuration.

    +
    +users
    + + +[]*./api/v1alpha1.User + + +
    +(Optional) +

    List of local users on the switch.

    +
    +grpc
    + + +GRPC + + +
    +(Optional) +

    Configuration for the gRPC server on the device. +Currently, only a single “default” gRPC server is supported.

    +
    +banner
    + + +TemplateSource + + +
    +(Optional) +

    MOTD banner to display on login.

    +
    +

    DeviceStatus +

    +

    +(Appears on:Device) +

    +
    +

    DeviceStatus defines the observed state of Device.

    +
    + + + + + + + + + + + + + + + + + +
    FieldDescription
    +phase
    + + +DevicePhase + + +
    +

    Phase represents the current phase of the Device.

    +
    +conditions
    + + +[]Kubernetes meta/v1.Condition + + +
    +(Optional) +

    The conditions are a list of status objects that describe the state of the Device.

    +
    +

    Endpoint +

    +

    +(Appears on:DeviceSpec) +

    +
    +
    + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +address
    + +string + +
    +

    Address is the management address of the device provided as ip:port.

    +
    +secretRef
    + + +Kubernetes core/v1.SecretReference + + +
    +(Optional) +

    SecretRef is name of the authentication secret for the device containing the username and password. +The secret must be of type kubernetes.io/basic-auth and as such contain the following keys: ‘username’ and ‘password’.

    +
    +tls
    + + +TLS + + +
    +(Optional) +

    Transport credentials for grpc connection to the switch.

    +
    +

    GNMI +

    +

    +(Appears on:GRPC) +

    +
    +
    + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +maxConcurrentCall
    + +byte + +
    +(Optional) +

    The maximum number of concurrent gNMI calls that can be made to the gRPC server on the switch for each VRF. +Configure a limit from 1 through 16. The default limit is 8.

    +
    +keepAliveTimeout
    + + +Kubernetes meta/v1.Duration + + +
    +(Optional) +

    Configure the keepalive timeout for inactive or unauthorized connections. +The gRPC agent is expected to periodically send an empty response to the client, on which the client is expected to respond with an empty request. +If the client does not respond within the keepalive timeout, the gRPC agent should close the connection. +The default interval value is 10 minutes.

    +
    +minSampleInterval
    + + +Kubernetes meta/v1.Duration + + +
    +(Optional) +

    Configure the minimum sample interval for the gNMI telemetry stream. +Once per stream sample interval, the switch sends the current values for all specified paths. +The default value is 10 seconds.

    +
    +

    GRPC +

    +

    +(Appears on:DeviceSpec) +

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +port
    + +int32 + +
    +(Optional) +

    The TCP port on which the gRPC server should listen. +The range of port-id is from 1024 to 65535. +Port 9339 is the default.

    +
    +certificateId
    + +string + +
    +(Optional) +

    Name of the certificate that is associated with the gRPC service. +The certificate is provisioned through other interfaces on the device, +such as e.g. the gNOI certificate management service.

    +
    +networkInstance
    + +string + +
    +(Optional) +

    Enable the gRPC agent to accept incoming (dial-in) RPC requests from a given network instance.

    +
    +gnmi
    + + +GNMI + + +
    +(Optional) +

    Additional gNMI configuration for the gRPC server. +This may not be supported by all devices.

    +
    +

    IPPrefix +

    +

    +(Appears on:ACLEntry) +

    +
    +

    IPPrefix represents an IP prefix in CIDR notation. +It is used to define a range of IP addresses in a network.

    +
    + + + + + + + + + + + + + +
    FieldDescription
    +-
    + + +net/netip.Prefix + + +
    +
    +

    Interface +

    +
    +

    Interface is the Schema for the interfaces API.

    +
    + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +metadata
    + + +Kubernetes meta/v1.ObjectMeta + + +
    +Refer to the Kubernetes API documentation for the fields of the +metadata field. +
    +spec
    + + +InterfaceSpec + + +
    +

    Specification of the desired state of the resource. +More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +deviceRef
    + + +LocalObjectReference + + +
    +

    DeviceName is the name of the Device this object belongs to. The Device object must exist in the same namespace. +Immutable.

    +
    +providerConfigRef
    + + +TypedLocalObjectReference + + +
    +(Optional) +

    ProviderConfigRef is a reference to a resource holding the provider-specific configuration of this interface. +This reference is used to link the Interface to its provider-specific configuration.

    +
    +name
    + +string + +
    +

    Name is the name of the interface.

    +
    +adminState
    + + +AdminState + + +
    +

    AdminState indicates whether the interface is administratively up or down.

    +
    +description
    + +string + +
    +(Optional) +

    Description provides a human-readable description of the interface.

    +
    +type
    + + +InterfaceType + + +
    +

    Type indicates the type of the interface.

    +
    +mtu
    + +int32 + +
    +(Optional) +

    MTU (Maximum Transmission Unit) specifies the size of the largest packet that can be sent over the interface.

    +
    +switchport
    + + +Switchport + + +
    +(Optional) +

    Switchport defines the switchport configuration for the interface. +This is only applicable for interfaces that are switchports (e.g., Ethernet interfaces).

    +
    +ipv4Addresses
    + +[]string + +
    +(Optional) +

    Ipv4Addresses is the list of IPv4 addresses assigned to the interface. +Each address should be given either in CIDR notation (e.g., “10.0.0.132”) +or as interface reference in the form of “unnumbered:” (e.g., “unnumbered:lo0”).

    +
    +
    +status,omitempty,omitzero
    + + +InterfaceStatus + + +
    +(Optional) +

    Status of the resource. This is set and updated automatically. +Read-only. +More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status

    +
    +

    InterfaceSpec +

    +

    +(Appears on:Interface) +

    +
    +

    InterfaceSpec defines the desired state of Interface.

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +deviceRef
    + + +LocalObjectReference + + +
    +

    DeviceName is the name of the Device this object belongs to. The Device object must exist in the same namespace. +Immutable.

    +
    +providerConfigRef
    + + +TypedLocalObjectReference + + +
    +(Optional) +

    ProviderConfigRef is a reference to a resource holding the provider-specific configuration of this interface. +This reference is used to link the Interface to its provider-specific configuration.

    +
    +name
    + +string + +
    +

    Name is the name of the interface.

    +
    +adminState
    + + +AdminState + + +
    +

    AdminState indicates whether the interface is administratively up or down.

    +
    +description
    + +string + +
    +(Optional) +

    Description provides a human-readable description of the interface.

    +
    +type
    + + +InterfaceType + + +
    +

    Type indicates the type of the interface.

    +
    +mtu
    + +int32 + +
    +(Optional) +

    MTU (Maximum Transmission Unit) specifies the size of the largest packet that can be sent over the interface.

    +
    +switchport
    + + +Switchport + + +
    +(Optional) +

    Switchport defines the switchport configuration for the interface. +This is only applicable for interfaces that are switchports (e.g., Ethernet interfaces).

    +
    +ipv4Addresses
    + +[]string + +
    +(Optional) +

    Ipv4Addresses is the list of IPv4 addresses assigned to the interface. +Each address should be given either in CIDR notation (e.g., “10.0.0.132”) +or as interface reference in the form of “unnumbered:” (e.g., “unnumbered:lo0”).

    +
    +

    InterfaceStatus +

    +

    +(Appears on:Interface) +

    +
    +

    InterfaceStatus defines the observed state of Interface.

    +
    + + + + + + + + + + + + + +
    FieldDescription
    +conditions
    + + +[]Kubernetes meta/v1.Condition + + +
    +(Optional) +

    The conditions are a list of status objects that describe the state of the Interface.

    +
    +

    InterfaceType +(string alias)

    +

    +(Appears on:InterfaceSpec) +

    +
    +

    InterfaceType represents the type of the interface.

    +
    + + + + + + + + + + + + +
    ValueDescription

    "Loopback"

    InterfaceTypeLoopback indicates that the interface is a loopback interface.

    +

    "Physical"

    InterfaceTypePhysical indicates that the interface is a physical/ethernet interface.

    +
    +

    LocalObjectReference +

    +

    +(Appears on:InterfaceSpec) +

    +
    +

    LocalObjectReference contains enough information to locate a +referenced object inside the same namespace.

    +
    + + + + + + + + + + + + + +
    FieldDescription
    +name
    + +string + +
    +

    Name of the referent. +More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names

    +
    +

    LogFacility +

    +
    +
    + + + + + + + + + + + + + + + + + +
    FieldDescription
    +name
    + +string + +
    +

    The name of the log facility.

    +
    +severity
    + + +Severity + + +
    +

    The severity level of the log messages for this facility.

    +
    +

    LogServer +

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +address
    + +string + +
    +

    IP address or hostname of the remote log server

    +
    +severity
    + + +Severity + + +
    +

    The servity level of the log messages sent to the server.

    +
    +networkInstance
    + +string + +
    +

    The network instance used to reach the log server.

    +
    +port
    + +int64 + +
    +(Optional) +

    The destination port number for syslog UDP messages to +the server. The default is 514.

    +
    +

    Logging +

    +

    +(Appears on:DeviceSpec) +

    +
    +
    + + + + + + + + + + + + + + + + + +
    FieldDescription
    +servers
    + + +[]*./api/v1alpha1.LogServer + + +
    +

    Servers is a list of remote log servers to which the device will send logs.

    +
    +facilities
    + + +[]*./api/v1alpha1.LogFacility + + +
    +

    Facilities is a list of log facilities to configure on the device.

    +
    +

    NTP +

    +

    +(Appears on:DeviceSpec) +

    +
    +
    + + + + + + + + + + + + + + + + + +
    FieldDescription
    +srcIf
    + +string + +
    +

    Source interface for all NTP traffic.

    +
    +servers
    + + +[]*./api/v1alpha1.NTPServer + + +
    +

    NTP servers.

    +
    +

    NTPServer +

    +
    +
    + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +address
    + +string + +
    +

    Hostname/IP address of the NTP server.

    +
    +prefer
    + +bool + +
    +(Optional) +

    Indicates whether this server should be preferred or not.

    +
    +networkInstance
    + +string + +
    +(Optional) +

    The network instance used to communicate with the NTP server.

    +
    +

    NameServer +

    +
    +
    + + + + + + + + + + + + + + + + + +
    FieldDescription
    +address
    + +string + +
    +

    The Hostname or IP address of the DNS server.

    +
    +networkInstance
    + +string + +
    +(Optional) +

    The network instance used to communicate with the DNS server.

    +
    +

    PKI +

    +

    +(Appears on:DeviceSpec) +

    +
    +
    + + + + + + + + + + + + + +
    FieldDescription
    +certificates
    + + +[]*./api/v1alpha1.Certificate + + +
    +

    Certificates is a list of certificates to be managed by the PKI.

    +
    +

    PasswordSource +

    +

    +(Appears on:User) +

    +
    +

    PasswordSource represents a source for the value of a password.

    +
    + + + + + + + + + + + + + +
    FieldDescription
    +secretKeyRef
    + + +Kubernetes core/v1.SecretKeySelector + + +
    +

    Selects a key of a secret.

    +
    +

    SNMP +

    +

    +(Appears on:DeviceSpec) +

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +contact
    + +string + +
    +

    The contact information for the SNMP server.

    +
    +location
    + +string + +
    +

    The location information for the SNMP server.

    +
    +engineId
    + +string + +
    +

    The SNMP engine ID for the SNMP server.

    +
    +srcIf
    + +string + +
    +

    Source interface to be used for sending out SNMP Trap/Inform notifications.

    +
    +communities
    + + +[]*./api/v1alpha1.SNMPCommunity + + +
    +(Optional) +

    SNMP communities for SNMPv1 or SNMPv2c.

    +
    +destinations
    + + +[]*./api/v1alpha1.SNMPDestination + + +
    +

    SNMP destinations for SNMP traps or informs.

    +
    +traps
    + +[]string + +
    +(Optional) +

    The list of trap groups to enable.

    +
    +

    SNMPCommunity +

    +
    +
    + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +name
    + +string + +
    +(Optional) +

    Name of the community.

    +
    +group
    + +string + +
    +(Optional) +

    Group to which the community belongs.

    +
    +acl
    + +string + +
    +(Optional) +

    ACL name to filter snmp requests.

    +
    +

    SNMPDestination +

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +address
    + +string + +
    +

    The Hostname or IP address of the SNMP host to send notifications to.

    +
    +type
    + +string + +
    +(Optional) +

    Type of message to send to host. Default is traps.

    +
    +version
    + +string + +
    +(Optional) +

    SNMP version. Default is v2c.

    +
    +target
    + +string + +
    +(Optional) +

    SNMP community or user name.

    +
    +networkInstance
    + +string + +
    +(Optional) +

    The network instance to use to source traffic.

    +
    +

    Severity +(string alias)

    +

    +(Appears on:LogFacility, LogServer) +

    +
    +

    Severity represents the severity level of a log message.

    +
    + + + + + + + + + + + + + + + + + + + + + + + + +
    ValueDescription

    "Alert"

    "Critical"

    "Debug"

    "Emergency"

    "Error"

    "Info"

    "Notice"

    "Warning"

    +

    Switchport +

    +

    +(Appears on:InterfaceSpec) +

    +
    +

    Switchport defines the switchport configuration for an interface.

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +mode
    + + +SwitchportMode + + +
    +

    Mode defines the switchport mode, such as access or trunk.

    +
    +accessVlan
    + +int32 + +
    +(Optional) +

    AccessVlan specifies the VLAN ID for access mode switchports. +Only applicable when Mode is set to “Access”.

    +
    +nativeVlan
    + +int32 + +
    +(Optional) +

    NativeVlan specifies the native VLAN ID for trunk mode switchports. +Only applicable when Mode is set to “Trunk”.

    +
    +allowedVlans
    + +[]int32 + +
    +(Optional) +

    AllowedVlans is a list of VLAN IDs that are allowed on the trunk port. +Only applicable when Mode is set to “Trunk”.

    +
    +

    SwitchportMode +(string alias)

    +

    +(Appears on:Switchport) +

    +
    +

    SwitchportMode represents the switchport mode of an interface.

    +
    + + + + + + + + + + + + +
    ValueDescription

    "Access"

    SwitchportModeAccess indicates that the switchport is in access mode.

    +

    "Trunk"

    SwitchportModeTrunk indicates that the switchport is in trunk mode.

    +
    +

    TLS +

    +

    +(Appears on:Endpoint) +

    +
    +
    + + + + + + + + + + + + + + + + + +
    FieldDescription
    +ca
    + + +Kubernetes core/v1.SecretKeySelector + + +
    +

    The CA certificate to verify the server’s identity.

    +
    +certificate
    + + +CertificateSource + + +
    +(Optional) +

    The client certificate and private key to use for mutual TLS authentication. +Leave empty if mTLS is not desired.

    +
    +

    TemplateSource +

    +

    +(Appears on:Bootstrap, DeviceSpec) +

    +
    +

    TemplateSource defines a source for template content. +It can be provided inline, or as a reference to a Secret or ConfigMap.

    +
    + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +inline
    + +string + +
    +(Optional) +

    Inline template content

    +
    +secretRef
    + + +Kubernetes core/v1.SecretKeySelector + + +
    +(Optional) +

    Reference to a Secret containing the template

    +
    +configMapRef
    + + +Kubernetes core/v1.ConfigMapKeySelector + + +
    +(Optional) +

    Reference to a ConfigMap containing the template

    +
    +

    TypedLocalObjectReference +

    +

    +(Appears on:InterfaceSpec) +

    +
    +

    TypedLocalObjectReference contains enough information to locate a +typed referenced object inside the same namespace.

    +
    + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +kind
    + +string + +
    +

    Kind of the resource being referenced. +Kind must consist of alphanumeric characters or ‘-’, start with an alphabetic character, and end with an alphanumeric character.

    +
    +name
    + +string + +
    +

    Name of the resource being referenced. +Name must consist of lower case alphanumeric characters, ‘-’ or ‘.’, and must start and end with an alphanumeric character.

    +
    +apiVersion
    + +string + +
    +

    APIVersion is the api group version of the resource being referenced.

    +
    +

    User +

    +
    +
    + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    +name
    + +string + +
    +

    Assigned username for this user.

    +
    +password
    + + +PasswordSource + + +
    +

    The user password, supplied as cleartext.

    +
    +role
    + +string + +
    +

    Role which the user is to be assigned to.

    +
    +
    +

    +Generated with gen-crd-api-reference-docs +

    diff --git a/go.mod b/go.mod index 1d56a098..1e0e8152 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,6 @@ require ( github.com/openconfig/goyang v1.6.2 github.com/openconfig/ygnmi v0.13.1-0.20250924235719-646562b5d0c3 github.com/openconfig/ygot v0.32.0 - github.com/sapcc/go-api-declarations v1.16.0 go.uber.org/automaxprocs v1.6.0 go.uber.org/zap v1.27.0 google.golang.org/grpc v1.71.0 diff --git a/go.sum b/go.sum index 2bcc6351..67af6884 100644 --- a/go.sum +++ b/go.sum @@ -137,8 +137,6 @@ github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoG github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sapcc/go-api-declarations v1.16.0 h1:uMB6SFPBAClusbAWwmeMeUi5eHVf9FpZWWf3VwPrsIg= -github.com/sapcc/go-api-declarations v1.16.0/go.mod h1:MWmLjmvjftgyAugNUfIhsDsHIzXH1pn32cWLZpiluKg= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= diff --git a/hack/api-reference/config.json b/hack/api-reference/config.json new file mode 100644 index 00000000..cf230e1c --- /dev/null +++ b/hack/api-reference/config.json @@ -0,0 +1,32 @@ +{ + "hideMemberFields": [ + "TypeMeta" + ], + "hideTypePatterns": [ + "ParseError$", + "List$" + ], + "externalPackages": [ + { + "typeMatchPrefix": "^net/netip", + "docsURLTemplate": "https://pkg.go.dev/net/netip#{{.TypeIdentifier}}" + }, + { + "typeMatchPrefix": "^k8s\\.io/apimachinery/pkg/api/resource", + "docsURLTemplate": "https://pkg.go.dev/k8s.io/apimachinery/pkg/api/resource#{{.TypeIdentifier}}" + }, + { + "typeMatchPrefix": "^k8s\\.io/apimachinery/pkg/types", + "docsURLTemplate": "https://pkg.go.dev/k8s.io/apimachinery/pkg/types#{{.TypeIdentifier}}" + }, + { + "typeMatchPrefix": "^k8s\\.io/(api|apimachinery/pkg/apis)/", + "docsURLTemplate": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#{{lower .TypeIdentifier}}-{{arrIndex .PackageSegments -1}}-{{arrIndex .PackageSegments -2}}" + } + ], + "typeDisplayNamePrefixOverrides": { + "k8s.io/api/": "Kubernetes ", + "k8s.io/apimachinery/pkg/apis/": "Kubernetes " + }, + "markdownDisabled": false +} diff --git a/hack/api-reference/template/members.tpl b/hack/api-reference/template/members.tpl new file mode 100644 index 00000000..a529c671 --- /dev/null +++ b/hack/api-reference/template/members.tpl @@ -0,0 +1,48 @@ +{{ define "members" }} + +{{ range .Members }} +{{ if not (hiddenMember .)}} + + + {{ fieldName . }}
    + + {{ if linkForType .Type }} + + {{ typeDisplayName .Type }} + + {{ else }} + {{ typeDisplayName .Type }} + {{ end }} + + + + {{ if fieldEmbedded . }} +

    + (Members of {{ fieldName . }} are embedded into this type.) +

    + {{ end}} + + {{ if isOptionalMember .}} + (Optional) + {{ end }} + + {{ safe (renderComments .CommentLines) }} + + {{ if and (eq (.Type.Name.Name) "ObjectMeta") }} + Refer to the Kubernetes API documentation for the fields of the + metadata field. + {{ end }} + + {{ if or (eq (fieldName .) "spec") }} +
    +
    + + {{ template "members" .Type }} +
    + {{ end }} + + +{{ end }} +{{ end }} + +{{ end }} diff --git a/hack/api-reference/template/pkg.tpl b/hack/api-reference/template/pkg.tpl new file mode 100644 index 00000000..14eae298 --- /dev/null +++ b/hack/api-reference/template/pkg.tpl @@ -0,0 +1,48 @@ +{{ define "packages" }} + +{{ with .packages}} +

    Packages:

    + +{{ end}} + +{{ range .packages }} +

    + {{- packageDisplayName . -}} +

    + + {{ with (index .GoPackages 0 )}} + {{ with .DocComments }} +
    + {{ safe (renderComments .) }} +
    + {{ end }} + {{ end }} + + Resource Types: +
      + {{- range (visibleTypes (sortedTypes .Types)) -}} + {{ if isExportedType . -}} +
    • + {{ typeDisplayName . }} +
    • + {{- end }} + {{- end -}} +
    + + {{ range (visibleTypes (sortedTypes .Types))}} + {{ template "type" . }} + {{ end }} +
    +{{ end }} + +

    + Generated with gen-crd-api-reference-docs +

    + +{{ end }} diff --git a/hack/api-reference/template/type.tpl b/hack/api-reference/template/type.tpl new file mode 100644 index 00000000..8f0d66b8 --- /dev/null +++ b/hack/api-reference/template/type.tpl @@ -0,0 +1,81 @@ +{{ define "type" }} + +

    + {{- .Name.Name }} + {{ if eq .Kind "Alias" }}({{.Underlying}} alias){{ end -}} +

    +{{ with (typeReferences .) }} +

    + (Appears on: + {{- $prev := "" -}} + {{- range . -}} + {{- if $prev -}}, {{ end -}} + {{- $prev = . -}} + {{ typeDisplayName . }} + {{- end -}} + ) +

    +{{ end }} + +
    + {{ safe (renderComments .CommentLines) }} +
    + +{{ with (constantsOfType .) }} + + + + + + + + + {{- range . -}} + + {{- /* + renderComments implicitly creates a

    element, so we + add one to the display name as well to make the contents + of the two cells align evenly. + */ -}} +

    + + + {{- end -}} + +
    ValueDescription

    {{ typeDisplayName . }}

    {{ safe (renderComments .CommentLines) }}
    +{{ end }} + +{{ if .Members }} + + + + + + + + + {{ if isExportedType . }} + + + + + + + + + {{ end }} + {{ template "members" .}} + +
    FieldDescription
    + apiVersion
    + string
    + + {{apiGroup .}} + +
    + kind
    + string +
    {{.Name.Name}}
    +{{ end }} + +{{ end }} diff --git a/hack/license-header.txt b/hack/license-header.txt new file mode 100644 index 00000000..3ceae8e8 --- /dev/null +++ b/hack/license-header.txt @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +SPDX-License-Identifier: Apache-2.0 diff --git a/hack/validate-kustomize.sh b/hack/validate-kustomize.sh new file mode 100755 index 00000000..41023e5a --- /dev/null +++ b/hack/validate-kustomize.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +set -e + +BASEDIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +export TERM="xterm-256color" + +bold="$(tput bold)" +red="$(tput setaf 1)" +green="$(tput setaf 2)" +normal="$(tput sgr0)" + +for kustomization in "$BASEDIR"/../config/**/kustomization.yaml; do + path="$(dirname "$kustomization")" + dir="$(realpath --relative-to "$BASEDIR"/.. "$path")" + echo "${bold}Validating $dir${normal}" + if ! kustomize_output="$(kustomize build "$path" 2>&1)"; then + echo "${red}Kustomize build $dir failed:" + echo "$kustomize_output" + exit 1 + fi + echo "${green}Successfully validated $dir${normal}" +done diff --git a/internal/provider/cisco/nxos/api/nxapi_test.go b/internal/provider/cisco/nxos/api/nxapi_test.go index 250183d4..6441b58a 100644 --- a/internal/provider/cisco/nxos/api/nxapi_test.go +++ b/internal/provider/cisco/nxos/api/nxapi_test.go @@ -13,6 +13,8 @@ import ( "github.com/ironcore-dev/network-operator/internal/provider/cisco/nxos/gnmiext" ) +const nxapiItems = "System/fm-items/nxapi-items" + func Test_NXAPI(t *testing.T) { nxapi := &NXAPI{ Enable: true, @@ -31,8 +33,8 @@ func Test_NXAPI(t *testing.T) { t.Errorf("expected value to be of type EditingUpdate") } - if update.XPath != "System/fm-items/nxapi-items" { - t.Errorf("expected key 'System/fm-items/nxapi-items' to be present") + if update.XPath != nxapiItems { + t.Errorf("expected key '" + nxapiItems + "' to be present") } i, ok := update.Value.(*nxos.Cisco_NX_OSDevice_System_FmItems_NxapiItems) @@ -64,8 +66,8 @@ func Test_NXAPI_Trustpoint(t *testing.T) { t.Errorf("expected value to be of type EditingUpdate") } - if update.XPath != "System/fm-items/nxapi-items" { - t.Errorf("expected key 'System/fm-items/nxapi-items' to be present") + if update.XPath != nxapiItems { + t.Errorf("expected key '" + nxapiItems + "' to be present") } i, ok := update.Value.(*nxos.Cisco_NX_OSDevice_System_FmItems_NxapiItems) @@ -122,8 +124,8 @@ func Test_NXAPI_Cert(t *testing.T) { t.Errorf("expected value to be of type EditingUpdate") } - if update.XPath != "System/fm-items/nxapi-items" { - t.Errorf("expected key 'System/fm-items/nxapi-items' to be present") + if update.XPath != nxapiItems { + t.Errorf("expected key '" + nxapiItems + "' to be present") } i, ok := update.Value.(*nxos.Cisco_NX_OSDevice_System_FmItems_NxapiItems) @@ -176,8 +178,8 @@ func Test_NXAPI_Disabled(t *testing.T) { t.Errorf("expected value to be of type EditingUpdate") } - if update.XPath != "System/fm-items/nxapi-items" { - t.Errorf("expected key 'System/fm-items/nxapi-items' to be present") + if update.XPath != nxapiItems { + t.Errorf("expected key '" + nxapiItems + "' to be present") } i, ok := update.Value.(*nxos.Cisco_NX_OSDevice_System_FmItems_NxapiItems) diff --git a/internal/provider/cisco/nxos/gnmiext/mock.go b/internal/provider/cisco/nxos/gnmiext/mock.go index ca1bb44b..8d6f9609 100644 --- a/internal/provider/cisco/nxos/gnmiext/mock.go +++ b/internal/provider/cisco/nxos/gnmiext/mock.go @@ -5,10 +5,11 @@ package gnmiext import ( "context" + "sync" + gpb "github.com/openconfig/gnmi/proto/gnmi" "github.com/openconfig/ygot/ygot" "google.golang.org/grpc" - "sync" ) // Ensure, that GNMIClientMock does implement GNMIClient. diff --git a/internal/provider/cisco/nxos/iface/loopback_test.go b/internal/provider/cisco/nxos/iface/loopback_test.go index 569b5995..10e57409 100644 --- a/internal/provider/cisco/nxos/iface/loopback_test.go +++ b/internal/provider/cisco/nxos/iface/loopback_test.go @@ -103,7 +103,6 @@ func Test_Loopback_ToYGOT_BaseConfig(t *testing.T) { } func Test_Loopback_ToYGOT_WithL3Config(t *testing.T) { - testAddressingL3Cfg, err := NewL3Config( WithNumberedAddressingIPv4([]string{"10.0.0.1/24"}), ) diff --git a/internal/provider/cisco/nxos/iface/portchannel_test.go b/internal/provider/cisco/nxos/iface/portchannel_test.go index 81a55dc2..d79004fc 100644 --- a/internal/provider/cisco/nxos/iface/portchannel_test.go +++ b/internal/provider/cisco/nxos/iface/portchannel_test.go @@ -276,6 +276,7 @@ func Test_PortChannel_ToYGOT(t *testing.T) { }) } } + func Test_PortChannel_Reset(t *testing.T) { tests := []struct { name string diff --git a/internal/provider/cisco/nxos/isis/interface.go b/internal/provider/cisco/nxos/isis/interface.go index e1b67cd3..db881072 100644 --- a/internal/provider/cisco/nxos/isis/interface.go +++ b/internal/provider/cisco/nxos/isis/interface.go @@ -16,9 +16,7 @@ import ( "github.com/ironcore-dev/network-operator/internal/provider/cisco/nxos/iface" ) -var ( - ErrUnsupported error = errors.New("isis: unsupported interface type for isis") -) +var ErrUnsupported error = errors.New("isis: unsupported interface type for isis") type Interface struct { name string // interface name, e.g., Ethernet1/1 @@ -76,6 +74,7 @@ func WithPointToPoint() IfOption { return nil } } + func WithBFD() IfOption { return func(i *Interface) error { i.bfd = true diff --git a/internal/provider/cisco/nxos/isis/interface_test.go b/internal/provider/cisco/nxos/isis/interface_test.go index f67b1c9d..87aa0b55 100644 --- a/internal/provider/cisco/nxos/isis/interface_test.go +++ b/internal/provider/cisco/nxos/isis/interface_test.go @@ -53,6 +53,7 @@ func Test_Interface_NewInterface(t *testing.T) { }) } } + func Test_Interface_toYGOT(t *testing.T) { tests := []struct { name string diff --git a/internal/provider/cisco/nxos/provider.go b/internal/provider/cisco/nxos/provider.go index 8f7efc83..ddf1aaea 100644 --- a/internal/provider/cisco/nxos/provider.go +++ b/internal/provider/cisco/nxos/provider.go @@ -108,7 +108,12 @@ func (p *Provider) CreateDevice(ctx context.Context, device *v1alpha1.Device) er if err != nil { return fmt.Errorf("failed to create grpc connection: %w", err) } - defer conn.Close() + defer func(conn *grpc.ClientConn) { + err := conn.Close() + if err != nil { + log.Error(err, "failed to close grpc connection") + } + }(conn) var opts []gnmiext.Option var isDryRun bool diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go index fb8d2330..dcb7aa4d 100644 --- a/test/e2e/e2e_suite_test.go +++ b/test/e2e/e2e_suite_test.go @@ -12,30 +12,27 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/ironcore-dev/network-operator/test/util" + "github.com/ironcore-dev/network-operator/test/utils" ) var ( // Optional Environment Variables: - // - PROMETHEUS_INSTALL_SKIP=true: Skips Prometheus Operator installation during test setup. // - CERT_MANAGER_INSTALL_SKIP=true: Skips CertManager installation during test setup. - // These variables are useful if Prometheus or CertManager is already installed, avoiding re-installation and conflicts. - skipPrometheusInstall = os.Getenv("PROMETHEUS_INSTALL_SKIP") == "true" + // These variables are useful if CertManager is already installed, avoiding + // re-installation and conflicts. skipCertManagerInstall = os.Getenv("CERT_MANAGER_INSTALL_SKIP") == "true" - // isPrometheusOperatorAlreadyInstalled will be set true when prometheus CRDs be found on the cluster - isPrometheusOperatorAlreadyInstalled = false // isCertManagerAlreadyInstalled will be set true when CertManager CRDs be found on the cluster isCertManagerAlreadyInstalled = false -) -// image is the name of the image which will be build and loaded -// with the code source changes to be tested. -const image = "ironcore.dev/network-operator:test" + // projectImage is the name of the image which will be build and loaded + // with the code source changes to be tested. + projectImage = "example.com/network-operator:v0.0.1" +) // TestE2E runs the end-to-end (e2e) test suite for the project. These tests execute in an isolated, -// temporary environment to validate project changes with the purposed to be used in CI jobs. +// temporary environment to validate project changes with the purpose of being used in CI jobs. // The default setup requires Kind, builds/loads the Manager Docker image locally, and installs -// CertManager and Prometheus Operator. +// CertManager. func TestE2E(t *testing.T) { RegisterFailHandler(Fail) _, _ = fmt.Fprintf(GinkgoWriter, "Starting network-operator integration test suite\n") @@ -43,42 +40,27 @@ func TestE2E(t *testing.T) { } var _ = BeforeSuite(func() { - By("Ensure that Prometheus is enabled") - cwd, err := util.GetProjectDir() - Expect(err).NotTo(HaveOccurred(), "Failed to get project directory") - - err = util.UncommentCode(cwd+"/config/default/kustomization.yaml", "#- ../prometheus", "#") - Expect(err).NotTo(HaveOccurred(), "Failed to enable Prometheus") - By("building the manager(Operator) image") - cmd := exec.Command("make", "docker-build", "IMG="+image) - _, err = util.Run(cmd) + cmd := exec.Command("make", "docker-build", fmt.Sprintf("IMG=%s", projectImage)) + _, err := utils.Run(cmd) ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to build the manager(Operator) image") + // TODO(user): If you want to change the e2e test vendor from Kind, ensure the image is + // built and available before running the tests. Also, remove the following block. By("loading the manager(Operator) image on Kind") - err = util.LoadImageToKindClusterWithName(image) + err = utils.LoadImageToKindClusterWithName(projectImage) ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to load the manager(Operator) image into Kind") // The tests-e2e are intended to run on a temporary cluster that is created and destroyed for testing. - // To prevent errors when tests run in environments with Prometheus or CertManager already installed, - // we check for their presence before execution. - // Setup Prometheus and CertManager before the suite if not skipped and if not already installed - if !skipPrometheusInstall { - By("checking if prometheus is installed already") - isPrometheusOperatorAlreadyInstalled = util.IsPrometheusCRDsInstalled() - if !isPrometheusOperatorAlreadyInstalled { - _, _ = fmt.Fprintf(GinkgoWriter, "Installing Prometheus Operator...\n") - Expect(util.InstallPrometheusOperator()).To(Succeed(), "Failed to install Prometheus Operator") - } else { - _, _ = fmt.Fprintf(GinkgoWriter, "WARNING: Prometheus Operator is already installed. Skipping installation...\n") - } - } + // To prevent errors when tests run in environments with CertManager already installed, + // we check for its presence before execution. + // Setup CertManager before the suite if not skipped and if not already installed if !skipCertManagerInstall { By("checking if cert manager is installed already") - isCertManagerAlreadyInstalled = util.IsCertManagerCRDsInstalled() + isCertManagerAlreadyInstalled = utils.IsCertManagerCRDsInstalled() if !isCertManagerAlreadyInstalled { _, _ = fmt.Fprintf(GinkgoWriter, "Installing CertManager...\n") - Expect(util.InstallCertManager()).To(Succeed(), "Failed to install CertManager") + Expect(utils.InstallCertManager()).To(Succeed(), "Failed to install CertManager") } else { _, _ = fmt.Fprintf(GinkgoWriter, "WARNING: CertManager is already installed. Skipping installation...\n") } @@ -86,13 +68,9 @@ var _ = BeforeSuite(func() { }) var _ = AfterSuite(func() { - // Teardown Prometheus and CertManager after the suite if not skipped and if they were not already installed - if !skipPrometheusInstall && !isPrometheusOperatorAlreadyInstalled { - _, _ = fmt.Fprintf(GinkgoWriter, "Uninstalling Prometheus Operator...\n") - util.UninstallPrometheusOperator() - } + // Teardown CertManager after the suite if not skipped and if it was not already installed if !skipCertManagerInstall && !isCertManagerAlreadyInstalled { _, _ = fmt.Fprintf(GinkgoWriter, "Uninstalling CertManager...\n") - util.UninstallCertManager() + utils.UninstallCertManager() } }) diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 85ad485e..39ce2f14 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -14,7 +14,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/ironcore-dev/network-operator/test/util" + "github.com/ironcore-dev/network-operator/test/utils" ) // namespace where the project is deployed in @@ -38,61 +38,54 @@ var _ = Describe("Manager", Ordered, func() { BeforeAll(func() { By("creating manager namespace") cmd := exec.Command("kubectl", "create", "ns", namespace) - _, err := util.Run(cmd) + _, err := utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to create namespace") By("labeling the namespace to enforce the restricted security policy") - cmd = exec.Command("kubectl", "label", "--overwrite", "ns", namespace, "pod-security.kubernetes.io/enforce=restricted") - _, err = util.Run(cmd) + cmd = exec.Command("kubectl", "label", "--overwrite", "ns", namespace, + "pod-security.kubernetes.io/enforce=restricted") + _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to label namespace with restricted policy") By("installing CRDs") - cmd = exec.Command("make", "deploy-crds") - _, err = util.Run(cmd) + cmd = exec.Command("make", "install") + _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to install CRDs") By("deploying the controller-manager") - cmd = exec.Command("make", "deploy", "IMG="+image) - _, err = util.Run(cmd) + cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", projectImage)) + _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to deploy the controller-manager") }) // After all tests have been executed, clean up by undeploying the controller, uninstalling CRDs, // and deleting the namespace. AfterAll(func() { - By("cleaning up the ClusterRoleBinding of the service account to allow access to metrics") - cmd := exec.Command("kubectl", "delete", "clusterrolebinding", metricsRoleBindingName) - _, err := util.Run(cmd) - Expect(err).NotTo(HaveOccurred(), "Failed to delete ClusterRoleBinding") - By("cleaning up the curl pod for metrics") - cmd = exec.Command("kubectl", "delete", "pod", "curl-metrics", "-n", namespace) - _, err = util.Run(cmd) - Expect(err).NotTo(HaveOccurred(), "Failed to delete curl-metrics pod") + cmd := exec.Command("kubectl", "delete", "pod", "curl-metrics", "-n", namespace) + _, _ = utils.Run(cmd) By("undeploying the controller-manager") cmd = exec.Command("make", "undeploy") - _, err = util.Run(cmd) - Expect(err).NotTo(HaveOccurred(), "Failed to undeploy the controller-manager") + _, _ = utils.Run(cmd) By("uninstalling CRDs") - cmd = exec.Command("make", "undeploy-crds") - _, err = util.Run(cmd) - Expect(err).NotTo(HaveOccurred(), "Failed to uninstall CRDs") + cmd = exec.Command("make", "uninstall") + _, _ = utils.Run(cmd) By("removing manager namespace") - cmd = exec.Command("kubectl", "delete", "ns", namespace, "--ignore-not-found") - _, err = util.Run(cmd) - Expect(err).NotTo(HaveOccurred(), "Failed to delete namespace") + cmd = exec.Command("kubectl", "delete", "ns", namespace) + _, _ = utils.Run(cmd) }) // After each test, check for failures and collect logs, events, // and pod descriptions for debugging. AfterEach(func() { - if specReport := CurrentSpecReport(); specReport.Failed() { + specReport := CurrentSpecReport() + if specReport.Failed() { By("Fetching controller manager pod logs") cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace) - controllerLogs, err := util.Run(cmd) + controllerLogs, err := utils.Run(cmd) if err == nil { _, _ = fmt.Fprintf(GinkgoWriter, "Controller logs:\n %s", controllerLogs) } else { @@ -101,7 +94,7 @@ var _ = Describe("Manager", Ordered, func() { By("Fetching Kubernetes events") cmd = exec.Command("kubectl", "get", "events", "-n", namespace, "--sort-by=.lastTimestamp") - eventsOutput, err := util.Run(cmd) + eventsOutput, err := utils.Run(cmd) if err == nil { _, _ = fmt.Fprintf(GinkgoWriter, "Kubernetes events:\n%s", eventsOutput) } else { @@ -110,7 +103,7 @@ var _ = Describe("Manager", Ordered, func() { By("Fetching curl-metrics logs") cmd = exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace) - metricsOutput, err := util.Run(cmd) + metricsOutput, err := utils.Run(cmd) if err == nil { _, _ = fmt.Fprintf(GinkgoWriter, "Metrics logs:\n %s", metricsOutput) } else { @@ -119,7 +112,7 @@ var _ = Describe("Manager", Ordered, func() { By("Fetching controller manager pod description") cmd = exec.Command("kubectl", "describe", "pod", controllerPodName, "-n", namespace) - podDescription, err := util.Run(cmd) + podDescription, err := utils.Run(cmd) if err == nil { fmt.Println("Pod description:\n", podDescription) } else { @@ -145,16 +138,19 @@ var _ = Describe("Manager", Ordered, func() { "-n", namespace, ) - podOutput, err := util.Run(cmd) + podOutput, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve controller-manager pod information") - podNames := util.GetNonEmptyLines(podOutput) + podNames := utils.GetNonEmptyLines(podOutput) g.Expect(podNames).To(HaveLen(1), "expected 1 controller pod running") controllerPodName = podNames[0] g.Expect(controllerPodName).To(ContainSubstring("controller-manager")) // Validate the pod's status - cmd = exec.Command("kubectl", "get", "pods", controllerPodName, "-o", "jsonpath={.status.phase}", "-n", namespace) - output, err := util.Run(cmd) + cmd = exec.Command("kubectl", "get", + "pods", controllerPodName, "-o", "jsonpath={.status.phase}", + "-n", namespace, + ) + output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred()) g.Expect(output).To(Equal("Running"), "Incorrect controller-manager pod status") } @@ -163,21 +159,18 @@ var _ = Describe("Manager", Ordered, func() { It("should ensure the metrics endpoint is serving metrics", func() { By("creating a ClusterRoleBinding for the service account to allow access to metrics") - // #nosec G204 - cmd := exec.Command("kubectl", "create", "clusterrolebinding", metricsRoleBindingName, "--clusterrole=network-operator-metrics-reader", fmt.Sprintf("--serviceaccount=%s:%s", namespace, serviceAccountName)) - _, err := util.Run(cmd) + cmd := exec.Command("kubectl", "create", "clusterrolebinding", metricsRoleBindingName, + "--clusterrole=network-operator-metrics-reader", + fmt.Sprintf("--serviceaccount=%s:%s", namespace, serviceAccountName), + ) + _, err := utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to create ClusterRoleBinding") By("validating that the metrics service is available") cmd = exec.Command("kubectl", "get", "service", metricsServiceName, "-n", namespace) - _, err = util.Run(cmd) + _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Metrics service should exist") - By("validating that the ServiceMonitor for Prometheus is applied in the namespace") - cmd = exec.Command("kubectl", "get", "ServiceMonitor", "-n", namespace) - _, err = util.Run(cmd) - Expect(err).NotTo(HaveOccurred(), "ServiceMonitor should exist") - By("getting the service account token") token, err := serviceAccountToken() Expect(err).NotTo(HaveOccurred()) @@ -185,8 +178,8 @@ var _ = Describe("Manager", Ordered, func() { By("waiting for the metrics endpoint to be ready") verifyMetricsEndpointReady := func(g Gomega) { - kcmd := exec.Command("kubectl", "get", "endpoints", metricsServiceName, "-n", namespace) - output, err := util.Run(kcmd) + cmd := exec.Command("kubectl", "get", "endpoints", metricsServiceName, "-n", namespace) + output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred()) g.Expect(output).To(ContainSubstring("8443"), "Metrics endpoint is not ready") } @@ -194,15 +187,15 @@ var _ = Describe("Manager", Ordered, func() { By("verifying that the controller manager is serving the metrics server") verifyMetricsServerStarted := func(g Gomega) { - kcmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace) - output, err := util.Run(kcmd) + cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace) + output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred()) - g.Expect(output).To(ContainSubstring("controller-runtime.metrics\tServing metrics server"), "Metrics server not yet started") + g.Expect(output).To(ContainSubstring("controller-runtime.metrics\tServing metrics server"), + "Metrics server not yet started") } Eventually(verifyMetricsServerStarted).Should(Succeed()) By("creating the curl-metrics pod to access the metrics endpoint") - // #nosec G204 cmd = exec.Command("kubectl", "run", "curl-metrics", "--restart=Never", "--namespace", namespace, "--image=curlimages/curl:latest", @@ -215,6 +208,7 @@ var _ = Describe("Manager", Ordered, func() { "command": ["/bin/sh", "-c"], "args": ["curl -v -k -H 'Authorization: Bearer %s' https://%s.%s.svc.cluster.local:8443/metrics"], "securityContext": { + "readOnlyRootFilesystem": true, "allowPrivilegeEscalation": false, "capabilities": { "drop": ["ALL"] @@ -226,16 +220,18 @@ var _ = Describe("Manager", Ordered, func() { } } }], - "serviceAccount": "%s" + "serviceAccountName": "%s" } }`, token, metricsServiceName, namespace, serviceAccountName)) - _, err = util.Run(cmd) + _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to create curl-metrics pod") By("waiting for the curl-metrics pod to complete.") verifyCurlUp := func(g Gomega) { - cmd := exec.Command("kubectl", "get", "pods", "curl-metrics", "-o", "jsonpath={.status.phase}", "-n", namespace) - output, err := util.Run(cmd) + cmd := exec.Command("kubectl", "get", "pods", "curl-metrics", + "-o", "jsonpath={.status.phase}", + "-n", namespace) + output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred()) g.Expect(output).To(Equal("Succeeded"), "curl pod in wrong status") } @@ -243,7 +239,9 @@ var _ = Describe("Manager", Ordered, func() { By("getting the metrics by checking curl-metrics logs") metricsOutput := getMetricsOutput() - Expect(metricsOutput).To(ContainSubstring("controller_runtime_webhook_panics_total")) + Expect(metricsOutput).To(ContainSubstring( + "controller_runtime_reconcile_total", + )) }) // +kubebuilder:scaffold:e2e-webhooks-checks @@ -263,14 +261,13 @@ var _ = Describe("Manager", Ordered, func() { // It uses the Kubernetes TokenRequest API to generate a token by directly sending a request // and parsing the resulting token from the API response. func serviceAccountToken() (string, error) { - // #nosec G101 const tokenRequestRawString = `{ "apiVersion": "authentication.k8s.io/v1", "kind": "TokenRequest" }` // Temporary file to store the token request - secretName := serviceAccountName + "-token-request" + secretName := fmt.Sprintf("%s-token-request", serviceAccountName) tokenRequestFile := filepath.Join("/tmp", secretName) err := os.WriteFile(tokenRequestFile, []byte(tokenRequestRawString), os.FileMode(0o644)) if err != nil { @@ -280,8 +277,12 @@ func serviceAccountToken() (string, error) { var out string verifyTokenCreation := func(g Gomega) { // Execute kubectl command to create the token - // #nosec G204 - cmd := exec.Command("kubectl", "create", "--raw", fmt.Sprintf("/api/v1/namespaces/%s/serviceaccounts/%s/token", namespace, serviceAccountName), "-f", tokenRequestFile) + cmd := exec.Command("kubectl", "create", "--raw", fmt.Sprintf( + "/api/v1/namespaces/%s/serviceaccounts/%s/token", + namespace, + serviceAccountName, + ), "-f", tokenRequestFile) + output, err := cmd.CombinedOutput() g.Expect(err).NotTo(HaveOccurred()) @@ -301,7 +302,7 @@ func serviceAccountToken() (string, error) { func getMetricsOutput() string { By("getting the curl-metrics logs") cmd := exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace) - metricsOutput, err := util.Run(cmd) + metricsOutput, err := utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod") Expect(metricsOutput).To(ContainSubstring("< HTTP/1.1 200 OK")) return metricsOutput diff --git a/test/gnmi/main.go b/test/gnmi/main.go index 024c5eef..81abf218 100644 --- a/test/gnmi/main.go +++ b/test/gnmi/main.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + package main import ( diff --git a/test/util/util.go b/test/utils/util.go similarity index 54% rename from test/util/util.go rename to test/utils/util.go index 174c09ea..2cf31a3d 100644 --- a/test/util/util.go +++ b/test/utils/util.go @@ -1,22 +1,24 @@ // SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors // SPDX-License-Identifier: Apache-2.0 -package util +package utils import ( - "bufio" - "bytes" "fmt" "os" "os/exec" "strings" - . "github.com/onsi/ginkgo/v2" //nolint:golint,revive + . "github.com/onsi/ginkgo/v2" // nolint:revive,staticcheck ) const ( - prometheusOperatorURL = "https://github.com/prometheus-operator/prometheus-operator/releases/download/v0.82.2/bundle.yaml" - certmanagerURL = "https://github.com/cert-manager/cert-manager/releases/download/v1.17.2/cert-manager.yaml" + prometheusOperatorVersion = "v0.77.1" + prometheusOperatorURL = "https://github.com/prometheus-operator/prometheus-operator/" + + "releases/download/%s/bundle.yaml" + + certmanagerVersion = "v1.16.3" + certmanagerURLTmpl = "https://github.com/cert-manager/cert-manager/releases/download/%s/cert-manager.yaml" ) func warnError(err error) { @@ -25,22 +27,19 @@ func warnError(err error) { // Run executes the provided command within this context func Run(cmd *exec.Cmd) (string, error) { - dir, err := GetProjectDir() - if err != nil { - return "", fmt.Errorf("failed to get project directory: %w", err) - } - + dir, _ := GetProjectDir() cmd.Dir = dir - if err = os.Chdir(cmd.Dir); err != nil { - _, _ = fmt.Fprintf(GinkgoWriter, "chdir dir: %s\n", err) + + if err := os.Chdir(cmd.Dir); err != nil { + _, _ = fmt.Fprintf(GinkgoWriter, "chdir dir: %q\n", err) } + cmd.Env = append(os.Environ(), "GO111MODULE=on") command := strings.Join(cmd.Args, " ") - _, _ = fmt.Fprintf(GinkgoWriter, "running: %s\n", command) - + _, _ = fmt.Fprintf(GinkgoWriter, "running: %q\n", command) output, err := cmd.CombinedOutput() if err != nil { - return string(output), fmt.Errorf("%s failed with error: (%w) %s", command, err, string(output)) + return string(output), fmt.Errorf("%q failed with error %q: %w", command, string(output), err) } return string(output), nil @@ -48,14 +47,16 @@ func Run(cmd *exec.Cmd) (string, error) { // InstallPrometheusOperator installs the prometheus Operator to be used to export the enabled metrics. func InstallPrometheusOperator() error { - cmd := exec.Command("kubectl", "create", "-f", prometheusOperatorURL) + url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion) + cmd := exec.Command("kubectl", "create", "-f", url) _, err := Run(cmd) return err } // UninstallPrometheusOperator uninstalls the prometheus func UninstallPrometheusOperator() { - cmd := exec.Command("kubectl", "delete", "-f", prometheusOperatorURL) + url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion) + cmd := exec.Command("kubectl", "delete", "-f", url) if _, err := Run(cmd); err != nil { warnError(err) } @@ -90,15 +91,30 @@ func IsPrometheusCRDsInstalled() bool { // UninstallCertManager uninstalls the cert manager func UninstallCertManager() { - cmd := exec.Command("kubectl", "delete", "-f", certmanagerURL) + url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion) + cmd := exec.Command("kubectl", "delete", "-f", url) if _, err := Run(cmd); err != nil { warnError(err) } + + // Delete leftover leases in kube-system (not cleaned by default) + kubeSystemLeases := []string{ + "cert-manager-cainjector-leader-election", + "cert-manager-controller", + } + for _, lease := range kubeSystemLeases { + cmd = exec.Command("kubectl", "delete", "lease", lease, + "-n", "kube-system", "--ignore-not-found", "--force", "--grace-period=0") + if _, err := Run(cmd); err != nil { + warnError(err) + } + } } // InstallCertManager installs the cert manager bundle. func InstallCertManager() error { - cmd := exec.Command("kubectl", "apply", "-f", certmanagerURL) + url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion) + cmd := exec.Command("kubectl", "apply", "-f", url) if _, err := Run(cmd); err != nil { return err } @@ -153,31 +169,8 @@ func LoadImageToKindClusterWithName(name string) error { if v, ok := os.LookupEnv("KIND_CLUSTER"); ok { cluster = v } - // See: https://kind.sigs.k8s.io/docs/user/rootless/#creating-a-kind-cluster-with-rootless-nerdctl - prov, ok := os.LookupEnv("KIND_EXPERIMENTAL_PROVIDER") - if ok && prov != "docker" { - // If kind is configured to not use the docker runtime (e.g. when using podman or nerctl), - // we need to create a temp file to store the image archive and load it as a tarball. - // See: https://github.com/kubernetes-sigs/kind/issues/2760 - file, err := os.CreateTemp("", "operator-image-") - if err != nil { - return fmt.Errorf("failed to create temp file: %w", err) - } - _ = file.Close() - defer func() { _ = os.Remove(file.Name()) }() - - // https://github.com/containerd/nerdctl/blob/main/docs/command-reference.md#whale-nerdctl-save - // https://docs.podman.io/en/v5.3.0/markdown/podman-save.1.html - cmd := exec.Command(prov, "save", name, "--output", file.Name()) - if _, err = Run(cmd); err != nil { - return fmt.Errorf("failed to save image: %w", err) - } - - cmd = exec.Command("kind", "load", "image-archive", file.Name(), "--name", cluster) //nolint:gosec - _, err = Run(cmd) - return err - } - cmd := exec.Command("kind", "load", "docker-image", name, "--name", cluster) + kindOptions := []string{"load", "docker-image", name, "--name", cluster} + cmd := exec.Command("kind", kindOptions...) _, err := Run(cmd) return err } @@ -192,6 +185,7 @@ func GetNonEmptyLines(output string) []string { res = append(res, element) } } + return res } @@ -199,55 +193,8 @@ func GetNonEmptyLines(output string) []string { func GetProjectDir() (string, error) { wd, err := os.Getwd() if err != nil { - return wd, err + return wd, fmt.Errorf("failed to get current working directory: %w", err) } wd = strings.ReplaceAll(wd, "/test/e2e", "") return wd, nil } - -// UncommentCode searches for target in the file and remove the comment prefix -// of the target content. The target content may span multiple lines. -func UncommentCode(filename, target, prefix string) error { - content, err := os.ReadFile(filename) - if err != nil { - return err - } - - idx := strings.Index(string(content), target) - if idx < 0 { - if strings.Contains(string(content), target[len(prefix):]) { - return nil // already uncommented - } - - return fmt.Errorf("unable to find the code %s to be uncomment", target) - } - - out := new(bytes.Buffer) - if _, err = out.Write(content[:idx]); err != nil { - return err - } - - scanner := bufio.NewScanner(bytes.NewBufferString(target)) - if !scanner.Scan() { - return nil - } - for { - _, err = out.WriteString(strings.TrimPrefix(scanner.Text(), prefix)) - if err != nil { - return err - } - // Avoid writing a newline in case the previous line was the last in target. - if !scanner.Scan() { - break - } - if _, err = out.WriteString("\n"); err != nil { - return err - } - } - - if _, err = out.Write(content[idx+len(target):]); err != nil { - return err - } - - return os.WriteFile(filename, out.Bytes(), 0o644) -}