diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ac07301 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +bin/ +testbin/ \ No newline at end of file diff --git a/.github/workflows/agent.yml b/.github/workflows/agent.yml new file mode 100644 index 0000000..99a072b --- /dev/null +++ b/.github/workflows/agent.yml @@ -0,0 +1,193 @@ +name: Build Cluster Agent v1 + +on: + push: + paths-ignore: + - 'v2/**' + - '.github/workflows/agentv2.yml' + - 'charts/ror-cluster-agent-v2/**' + # Publish semver tags as releases. + #tags: [ 'v*.*.*' ] + pull_request: + branches: + - main + + +env: + # Use docker.io for Docker Hub if empty + REGISTRY: ghcr.io + # github.repository as / + # test + IMAGE_NAME: norskhelsenett/ror-cluster-agent + GH_TOKEN: ${{ secrets.VARTOKEN }} + +jobs: + bump-version: + runs-on: ubuntu-latest + outputs: + ror_version: ${{ steps.set_version.outputs.ror_version }} + steps: + - uses: actions/checkout@v4 + - id: set_version + run: | + PREV_VERSION=$(gh variable get V1VERSION) + ROR_VERSION=$(echo $PREV_VERSION | awk -F. '{$NF = $NF + 1;} 1' | sed 's/ /./g') + echo "ror_version=$ROR_VERSION" >> "$GITHUB_OUTPUT" + gh variable set V1VERSION --body $ROR_VERSION + echo "version bumped from $PREV_VERSION to $ROR_VERSION" + build-app: + runs-on: ubuntu-latest + needs: bump-version + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.23' + - name: Cache Go modules + uses: actions/cache@v3 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: Build + run: | + echo $ROR_VERSION + go get ./... + mkdir -p dist/isbuilt + CGO_ENABLED=0 go build -o dist/agent -ldflags "-w -extldflags '-static' -X internal/config.version=$ROR_VERSION -X internal/config.commit=$CI_COMMIT_SHORT_SHA" cmd/agent/main.go + touch dist/isbuilt/agent + env: + ROR_VERSION: ${{ needs.bump-version.outputs.ror_version }} + + - name: Archive binary + uses: actions/upload-artifact@v4 + with: + name: binary-build + path: | + dist/agent + dist/isbuilt/agent + retention-days: 1 + + + build-container-image: + runs-on: ubuntu-latest + #if: ${{ ! startsWith(github.ref, 'refs/tags/') }} + needs: + - build-app + - bump-version + permissions: + contents: read + packages: write + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Download binary build artifacts + uses: actions/download-artifact@v4 + + - name: Move artifacts + run: | + mv binary-build dist + + - name: Install cosign + if: github.event_name != 'pull_request' + uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 #v3.5.0 + with: + cosign-release: 'v2.2.4' + + # Set up BuildKit Docker container builder to be able to build + # multi-platform images and export cache + # https://github.com/docker/setup-buildx-action + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 + + # Login against a Docker registry except on PR + # https://github.com/docker/login-action + - name: Log into registry ${{ env.REGISTRY }} + if: github.event_name != 'pull_request' + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Extract metadata (tags, labels) for Docker + # https://github.com/docker/metadata-action + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=latest + type=raw,value=latestv1 + type=raw,value=${{ env.ROR_VERSION }} + env: + ROR_VERSION: ${{ needs.bump-version.outputs.ror_version }} + + # Build and push Docker image with Buildx (don't push on PR) + # https://github.com/docker/build-push-action + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + # Sign the resulting Docker image digest except on PRs. + # This will only write to the public Rekor transparency log when the Docker + # repository is public to avoid leaking data. If you would like to publish + # transparency data even for private images, pass --force to cosign below. + # https://github.com/sigstore/cosign + - name: Sign the published Docker image + if: ${{ github.event_name != 'pull_request' }} + env: + # https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable + TAGS: ${{ steps.meta.outputs.tags }} + DIGEST: ${{ steps.build-and-push.outputs.digest }} + # This step uses the identity token to provision an ephemeral certificate + # against the sigstore community Fulcio instance. + run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} + publish-helm: + runs-on: ubuntu-latest + needs: + - bump-version + - build-container-image + env: + ROR_VERSION: ${{ needs.bump-version.outputs.ror_version }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install helm + uses: azure/setup-helm@v1 + with: + version: v3.15.0 + + - name: install-yq + run: | + wget https://github.com/mikefarah/yq/releases/download/${VERSION}/${BINARY}.tar.gz -O - | tar xz && mv ${BINARY} yq && chmod +x yq + env: + VERSION: v4.44.5 + BINARY: yq_linux_amd64 + + - name: Build helm chart + run: | + ./yq e -i '.version = strenv(ROR_VERSION),.appVersion = strenv(ROR_VERSION)' charts/ror-cluster-agent-v1/Chart.yaml + ./yq e -i '.image.tag = strenv(ROR_VERSION)' charts/ror-cluster-agent-v1/values.yaml + ./yq e -i '.image.repository = "ncr.sky.nhn.no/ror/cluster-agent"' charts/ror-cluster-agent-v1/values.yaml + helm package charts/ror-cluster-agent-v1 + echo ${{ secrets.GITHUB_TOKEN }} | helm registry login -u ${{ github.actor }} ${{ env.REGISTRY }} --password-stdin + helm push ror-cluster-agent-${ROR_VERSION}.tgz oci://${{ env.REGISTRY }}/norskhelsenett/helm/ + diff --git a/.github/workflows/agentv2.yml b/.github/workflows/agentv2.yml new file mode 100644 index 0000000..748b9f8 --- /dev/null +++ b/.github/workflows/agentv2.yml @@ -0,0 +1,192 @@ +name: Build Cluster Agent v2 + +on: + push: + paths: + - v2/** + - .github/workflows/agentv2.yml + - 'charts/ror-cluster-agent-v2/**' + # Publish semver tags as releases. + #tags: [ 'v*.*.*' ] + pull_request: + branches: + - main + + +env: + # Use docker.io for Docker Hub if empty + REGISTRY: ghcr.io + # github.repository as / + # test + IMAGE_NAME: norskhelsenett/ror-cluster-agent + GH_TOKEN: ${{ secrets.VARTOKEN }} + +jobs: + + bump-version: + runs-on: ubuntu-latest + outputs: + ror_version: ${{ steps.set_version.outputs.ror_version }} + steps: + - uses: actions/checkout@v4 + - id: set_version + run: | + PREV_VERSION=$(gh variable get V2VERSION) + ROR_VERSION=$(echo $PREV_VERSION | awk -F. '{$NF = $NF + 1;} 1' | sed 's/ /./g') + echo "ror_version=$ROR_VERSION" >> "$GITHUB_OUTPUT" + gh variable set V2VERSION --body $ROR_VERSION + echo "version bumped from $PREV_VERSION to $ROR_VERSION" + build-app: + runs-on: ubuntu-latest + needs: bump-version + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.23' + - name: Cache Go modules + uses: actions/cache@v3 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: Build + run: | + echo $ROR_VERSION + cd v2 + go get ./... + mkdir -p dist/isbuilt + CGO_ENABLED=0 go build -o dist/agent -ldflags "-w -extldflags '-static' -X internal/agentconfig.version=$ROR_VERSION -X internal/agentconfig.commit=$CI_COMMIT_SHORT_SHA" cmd/agent/main.go + touch dist/isbuilt/agentv2 + env: + ROR_VERSION: ${{ needs.bump-version.outputs.ror_version }} + - name: Archive binary + uses: actions/upload-artifact@v4 + with: + name: binary-build + path: | + v2/dist/agent + v2/dist/isbuilt/agentv2 + retention-days: 1 + + + build-container-image: + runs-on: ubuntu-latest + #if: ${{ ! startsWith(github.ref, 'refs/tags/') }} + needs: + - build-app + - bump-version + permissions: + contents: read + packages: write + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Download binary build artifacts + uses: actions/download-artifact@v4 + + - name: Move artifacts + run: | + mv binary-build v2/dist + + - name: Install cosign + if: github.event_name != 'pull_request' + uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 #v3.5.0 + with: + cosign-release: 'v2.2.4' + + # Set up BuildKit Docker container builder to be able to build + # multi-platform images and export cache + # https://github.com/docker/setup-buildx-action + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 + + # Login against a Docker registry except on PR + # https://github.com/docker/login-action + - name: Log into registry ${{ env.REGISTRY }} + if: github.event_name != 'pull_request' + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Extract metadata (tags, labels) for Docker + # https://github.com/docker/metadata-action + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=latestv2 + type=raw,value=${{ env.ROR_VERSION }} + env: + ROR_VERSION: ${{ needs.bump-version.outputs.ror_version }} + + # Build and push Docker image with Buildx (don't push on PR) + # https://github.com/docker/build-push-action + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0 + with: + context: v2 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + # Sign the resulting Docker image digest except on PRs. + # This will only write to the public Rekor transparency log when the Docker + # repository is public to avoid leaking data. If you would like to publish + # transparency data even for private images, pass --force to cosign below. + # https://github.com/sigstore/cosign + - name: Sign the published Docker image + if: ${{ github.event_name != 'pull_request' }} + env: + # https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable + TAGS: ${{ steps.meta.outputs.tags }} + DIGEST: ${{ steps.build-and-push.outputs.digest }} + # This step uses the identity token to provision an ephemeral certificate + # against the sigstore community Fulcio instance. + run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} + publish-helm: + runs-on: ubuntu-latest + needs: + - build-container-image + - bump-version + env: + ROR_VERSION: ${{ needs.bump-version.outputs.ror_version }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install helm + uses: azure/setup-helm@v1 + with: + version: v3.15.0 + + - name: install-yq + run: | + wget https://github.com/mikefarah/yq/releases/download/${VERSION}/${BINARY}.tar.gz -O - | tar xz && mv ${BINARY} yq && chmod +x yq + env: + VERSION: v4.44.5 + BINARY: yq_linux_amd64 + + - name: Build helm chart + run: | + ./yq e -i '.version = strenv(ROR_VERSION),.appVersion = strenv(ROR_VERSION)' charts/ror-cluster-agent-v2/Chart.yaml + ./yq e -i '.image.tag = strenv(ROR_VERSION)' charts/ror-cluster-agent-v2/values.yaml + ./yq e -i '.image.repository = "ncr.sky.nhn.no/ror/cluster-agent"' charts/ror-cluster-agent-v2/values.yaml + helm package charts/ror-cluster-agent-v2 + echo ${{ secrets.GITHUB_TOKEN }} | helm registry login -u ${{ github.actor }} ${{ env.REGISTRY }} --password-stdin + helm push ror-cluster-agent-${ROR_VERSION}.tgz oci://${{ env.REGISTRY }}/norskhelsenett/helm/ diff --git a/.gitignore b/.gitignore index 6f72f89..d6acaaa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,25 +1,2 @@ -# If you prefer the allow list template instead of the deny list, see community template: -# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore -# -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, built with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - -# Dependency directories (remove the comment below to include it) -# vendor/ - -# Go workspace file -go.work -go.work.sum - -# env file -.env +dist/agent +.vscode/launch.json diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8b5419a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +ARG GCR_MIRROR=gcr.io/ +FROM ${GCR_MIRROR}distroless/static:nonroot +LABEL org.opencontainers.image.source https://github.com/norskhelsenett/ror-agent +LABEL org.opencontainers.image.description ROR Agent v1 +WORKDIR / + +COPY cmd/agent/version.json /version.json +COPY dist/agent /bin/ror-agent +USER 10000:10000 +EXPOSE 8100 + +ENTRYPOINT ["/bin/ror-agent"] + diff --git a/Dockerfile.compose b/Dockerfile.compose new file mode 100644 index 0000000..33fca33 --- /dev/null +++ b/Dockerfile.compose @@ -0,0 +1,15 @@ +ARG DOCKER_MIRROR=docker.io/ +ARG GCR_MIRROR=gcr.io/ +FROM ${DOCKER_MIRROR}golang1.23-alpine as builder +WORKDIR /app +COPY . . +RUN go get ./... + +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o dist/ror-agent cmd/agent/main.go + +FROM ${GCR_MIRROR}distroless/static:nonroot +WORKDIR / +COPY --from=builder /app/dist/ror-agent . +USER 10000:10000 + +ENTRYPOINT ["/ror-agent"] diff --git a/README.md b/README.md index 50928a8..07e7d61 100644 --- a/README.md +++ b/README.md @@ -1 +1,48 @@ -# ror-agent \ No newline at end of file +# NHN-ROR-Agent + +K8s agent for NHN-Ror to report from clusters inside Privat Sky. + +## Prerequisites + +1. Golang 1.20.x or newer [GoDev](https://go.dev/dl) +2. OCI image builder (docker, podman, etc) + +## Connect to cluster + +- Locally ror-agent uses the `%userprofile%/.kube/config` as default to connect to the cluster. + +### Create test cluster with k3d (https://k3d.io) + +```bash +k3d cluster create k8s --api-port 65001 -p "10081:80@loadbalancer" --agents 2 +``` + +Spinning up a cluster in docker-desktop, with a loadbalancer and 2 agents. [More info](https://k3d.io/v5.4.1/usage/exposing_services/) + +# Get started, and run it + +- Open repo root in your favorite IDE (VS Code, VS, Rider, etc) +- Open a terminal +- go to `/src/clients/ror-agent` +- `go get` (install dependencies) +- `go build -o agent` -> results in a executable file (win: agent.exe, unix: agent) +- run `agent` + +# Debug in Visual Studio Code + +- Open `` as workspace/folder in VS Code +- Open terminal, go to `/src/clients/ror-agent`, and run `go get` +- Go to the debug button on the sidebar in VS Code +- Start `Debug Ror-agent` configuration +- Set your breakpoints in the code + +# Health endpoint + +Go to [health endpoint: https://localhost:8090/health](https://localhost:8090/health) to check health + +# Trigger add/update/delete ingress changes + +- Open terminal +- Go to `\testdata` +- Apply test ingress manifest + - `kubectl apply -f avi-ingress-external.yaml` diff --git a/charts/ror-cluster-agent-v1/.helmignore b/charts/ror-cluster-agent-v1/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/charts/ror-cluster-agent-v1/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/ror-cluster-agent-v1/Chart.yaml b/charts/ror-cluster-agent-v1/Chart.yaml new file mode 100644 index 0000000..8f47977 --- /dev/null +++ b/charts/ror-cluster-agent-v1/Chart.yaml @@ -0,0 +1,25 @@ +apiVersion: v2 +name: ror-cluster-agent +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.2 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "0.9.1" +icon: https://raw.githubusercontent.com/NorskHelsenett/ror/main/media/ror.svg \ No newline at end of file diff --git a/charts/ror-cluster-agent-v1/templates/_helpers.tpl b/charts/ror-cluster-agent-v1/templates/_helpers.tpl new file mode 100644 index 0000000..168e0e4 --- /dev/null +++ b/charts/ror-cluster-agent-v1/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "ror-cluster-agent.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "ror-cluster-agent.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "ror-cluster-agent.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "ror-cluster-agent.labels" -}} +helm.sh/chart: {{ include "ror-cluster-agent.chart" . }} +{{ include "ror-cluster-agent.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "ror-cluster-agent.selectorLabels" -}} +app.kubernetes.io/name: "ror-cluster-agent" +app.kubernetes.io/instance: "ror-cluster-agent" +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "ror-cluster-agent.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "ror-cluster-agent.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/charts/ror-cluster-agent-v1/templates/clusterrole.yaml b/charts/ror-cluster-agent-v1/templates/clusterrole.yaml new file mode 100644 index 0000000..231df1c --- /dev/null +++ b/charts/ror-cluster-agent-v1/templates/clusterrole.yaml @@ -0,0 +1,8 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "ror-cluster-agent.fullname" . }}:config-read-cr +rules: +- apiGroups: ["*"] + resources: ["*"] + verbs: ["get", "watch", "list"] diff --git a/charts/ror-cluster-agent-v1/templates/deployment.yaml b/charts/ror-cluster-agent-v1/templates/deployment.yaml new file mode 100644 index 0000000..f64b3a5 --- /dev/null +++ b/charts/ror-cluster-agent-v1/templates/deployment.yaml @@ -0,0 +1,71 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "ror-cluster-agent.fullname" . }} + labels: + {{- include "ror-cluster-agent.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "ror-cluster-agent.selectorLabels" . | nindent 6 }} + revisionHistoryLimit: 2 + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "ror-cluster-agent.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "ror-cluster-agent.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: LOG_LEVEL + value: {{ .Values.debuglevel }} + - name: ROR_URL + value: {{ .Values.api }} + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: SECRET_NAME + value: {{ .Values.secretname }} + ports: + - name: liveness-probe + containerPort: 8100 + protocol: TCP + livenessProbe: + httpGet: + path: /health + port: 8100 + readinessProbe: + httpGet: + path: /health + port: 8100 + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/ror-cluster-agent-v1/templates/role.yaml b/charts/ror-cluster-agent-v1/templates/role.yaml new file mode 100644 index 0000000..7b246df --- /dev/null +++ b/charts/ror-cluster-agent-v1/templates/role.yaml @@ -0,0 +1,10 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + namespace: {{.Release.Namespace}} + name: {{ include "ror-cluster-agent.fullname" . }}:secret-role +rules: +- apiGroups: [""] # "" indicates the core API group + resources: ["secrets"] + verbs: ["get", "watch", "list","create", "update", "patch", "delete"] \ No newline at end of file diff --git a/charts/ror-cluster-agent-v1/templates/rolebinding.yaml b/charts/ror-cluster-agent-v1/templates/rolebinding.yaml new file mode 100644 index 0000000..1b56d60 --- /dev/null +++ b/charts/ror-cluster-agent-v1/templates/rolebinding.yaml @@ -0,0 +1,40 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: ror-cluster-agent:config-reader-crb +subjects: +- kind: ServiceAccount + name: {{ include "ror-cluster-agent.serviceAccountName" . }} + namespace: {{.Release.Namespace}} +roleRef: + kind: ClusterRole + name: {{ include "ror-cluster-agent.fullname" . }}:config-read-cr + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "ror-cluster-agent.fullname" . }}-restricted-psp + namespace: {{.Release.Namespace}} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: psp:vmware-system-restricted +subjects: +- kind: ServiceAccount + name: {{ include "ror-cluster-agent.serviceAccountName" . }} + namespace: {{.Release.Namespace}} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "ror-cluster-agent.fullname" . }}:secret-rb + namespace: {{.Release.Namespace}} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "ror-cluster-agent.fullname" . }}:secret-role +subjects: +- kind: ServiceAccount + name: {{ include "ror-cluster-agent.serviceAccountName" . }} + namespace: {{.Release.Namespace}} \ No newline at end of file diff --git a/charts/ror-cluster-agent-v1/templates/serviceaccount.yaml b/charts/ror-cluster-agent-v1/templates/serviceaccount.yaml new file mode 100644 index 0000000..ca28a78 --- /dev/null +++ b/charts/ror-cluster-agent-v1/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "ror-cluster-agent.serviceAccountName" . }} + namespace: {{.Release.Namespace}} + labels: + {{- include "ror-cluster-agent.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/ror-cluster-agent-v1/values.yaml b/charts/ror-cluster-agent-v1/values.yaml new file mode 100644 index 0000000..e259f43 --- /dev/null +++ b/charts/ror-cluster-agent-v1/values.yaml @@ -0,0 +1,39 @@ +debuglevel: INFO +replicaCount: 1 +api: https://api.ror.sky.test.nhn.no +secretname: ror-secret +image: + repository: ghcr.io/norskhelsenett/ror-cluster-agent + pullPolicy: Always + tag: "1.0.1" +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "ror-cluster-agent" +serviceAccount: + create: true + name: ror-cluster-agent-sa +podAnnotations: {} +podSecurityContext: + runAsNonRoot: true + fsGroup: 2000 + runAsUser: 1001 + runAsGroup: 1001 + supplementalGroups: [501] +securityContext: + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + seccompProfile: + type: RuntimeDefault + capabilities: + drop: + - ALL +resources: + limits: + cpu: 1000m + memory: 256Mi + requests: + cpu: 100m + memory: 128Mi +nodeSelector: {} +tolerations: [] +affinity: {} diff --git a/charts/ror-cluster-agent-v2/.helmignore b/charts/ror-cluster-agent-v2/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/charts/ror-cluster-agent-v2/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/ror-cluster-agent-v2/Chart.yaml b/charts/ror-cluster-agent-v2/Chart.yaml new file mode 100644 index 0000000..8f47977 --- /dev/null +++ b/charts/ror-cluster-agent-v2/Chart.yaml @@ -0,0 +1,25 @@ +apiVersion: v2 +name: ror-cluster-agent +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.2 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "0.9.1" +icon: https://raw.githubusercontent.com/NorskHelsenett/ror/main/media/ror.svg \ No newline at end of file diff --git a/charts/ror-cluster-agent-v2/templates/_helpers.tpl b/charts/ror-cluster-agent-v2/templates/_helpers.tpl new file mode 100644 index 0000000..168e0e4 --- /dev/null +++ b/charts/ror-cluster-agent-v2/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "ror-cluster-agent.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "ror-cluster-agent.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "ror-cluster-agent.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "ror-cluster-agent.labels" -}} +helm.sh/chart: {{ include "ror-cluster-agent.chart" . }} +{{ include "ror-cluster-agent.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "ror-cluster-agent.selectorLabels" -}} +app.kubernetes.io/name: "ror-cluster-agent" +app.kubernetes.io/instance: "ror-cluster-agent" +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "ror-cluster-agent.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "ror-cluster-agent.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/charts/ror-cluster-agent-v2/templates/clusterrole.yaml b/charts/ror-cluster-agent-v2/templates/clusterrole.yaml new file mode 100644 index 0000000..231df1c --- /dev/null +++ b/charts/ror-cluster-agent-v2/templates/clusterrole.yaml @@ -0,0 +1,8 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "ror-cluster-agent.fullname" . }}:config-read-cr +rules: +- apiGroups: ["*"] + resources: ["*"] + verbs: ["get", "watch", "list"] diff --git a/charts/ror-cluster-agent-v2/templates/deployment.yaml b/charts/ror-cluster-agent-v2/templates/deployment.yaml new file mode 100644 index 0000000..f64b3a5 --- /dev/null +++ b/charts/ror-cluster-agent-v2/templates/deployment.yaml @@ -0,0 +1,71 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "ror-cluster-agent.fullname" . }} + labels: + {{- include "ror-cluster-agent.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "ror-cluster-agent.selectorLabels" . | nindent 6 }} + revisionHistoryLimit: 2 + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "ror-cluster-agent.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "ror-cluster-agent.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: LOG_LEVEL + value: {{ .Values.debuglevel }} + - name: ROR_URL + value: {{ .Values.api }} + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: SECRET_NAME + value: {{ .Values.secretname }} + ports: + - name: liveness-probe + containerPort: 8100 + protocol: TCP + livenessProbe: + httpGet: + path: /health + port: 8100 + readinessProbe: + httpGet: + path: /health + port: 8100 + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/ror-cluster-agent-v2/templates/role.yaml b/charts/ror-cluster-agent-v2/templates/role.yaml new file mode 100644 index 0000000..7b246df --- /dev/null +++ b/charts/ror-cluster-agent-v2/templates/role.yaml @@ -0,0 +1,10 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + namespace: {{.Release.Namespace}} + name: {{ include "ror-cluster-agent.fullname" . }}:secret-role +rules: +- apiGroups: [""] # "" indicates the core API group + resources: ["secrets"] + verbs: ["get", "watch", "list","create", "update", "patch", "delete"] \ No newline at end of file diff --git a/charts/ror-cluster-agent-v2/templates/rolebinding.yaml b/charts/ror-cluster-agent-v2/templates/rolebinding.yaml new file mode 100644 index 0000000..1b56d60 --- /dev/null +++ b/charts/ror-cluster-agent-v2/templates/rolebinding.yaml @@ -0,0 +1,40 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: ror-cluster-agent:config-reader-crb +subjects: +- kind: ServiceAccount + name: {{ include "ror-cluster-agent.serviceAccountName" . }} + namespace: {{.Release.Namespace}} +roleRef: + kind: ClusterRole + name: {{ include "ror-cluster-agent.fullname" . }}:config-read-cr + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "ror-cluster-agent.fullname" . }}-restricted-psp + namespace: {{.Release.Namespace}} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: psp:vmware-system-restricted +subjects: +- kind: ServiceAccount + name: {{ include "ror-cluster-agent.serviceAccountName" . }} + namespace: {{.Release.Namespace}} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "ror-cluster-agent.fullname" . }}:secret-rb + namespace: {{.Release.Namespace}} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "ror-cluster-agent.fullname" . }}:secret-role +subjects: +- kind: ServiceAccount + name: {{ include "ror-cluster-agent.serviceAccountName" . }} + namespace: {{.Release.Namespace}} \ No newline at end of file diff --git a/charts/ror-cluster-agent-v2/templates/serviceaccount.yaml b/charts/ror-cluster-agent-v2/templates/serviceaccount.yaml new file mode 100644 index 0000000..ca28a78 --- /dev/null +++ b/charts/ror-cluster-agent-v2/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "ror-cluster-agent.serviceAccountName" . }} + namespace: {{.Release.Namespace}} + labels: + {{- include "ror-cluster-agent.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/ror-cluster-agent-v2/values.yaml b/charts/ror-cluster-agent-v2/values.yaml new file mode 100644 index 0000000..1f9e98c --- /dev/null +++ b/charts/ror-cluster-agent-v2/values.yaml @@ -0,0 +1,39 @@ +debuglevel: INFO +replicaCount: 1 +api: https://api.ror.sky.test.nhn.no +secretname: ror-secret +image: + repository: ghcr.io/norskhelsenett/ror-cluster-agent + pullPolicy: Always + tag: "2.0.1" +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "ror-cluster-agent-v2" +serviceAccount: + create: false + name: ror-cluster-agent-sa +podAnnotations: {} +podSecurityContext: + runAsNonRoot: true + fsGroup: 2000 + runAsUser: 1001 + runAsGroup: 1001 + supplementalGroups: [501] +securityContext: + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + seccompProfile: + type: RuntimeDefault + capabilities: + drop: + - ALL +resources: + limits: + cpu: 1000m + memory: 256Mi + requests: + cpu: 100m + memory: 128Mi +nodeSelector: {} +tolerations: [] +affinity: {} diff --git a/cmd/agent/main.go b/cmd/agent/main.go new file mode 100644 index 0000000..0b378fc --- /dev/null +++ b/cmd/agent/main.go @@ -0,0 +1,148 @@ +package main + +import ( + "context" + "fmt" + "os" + "os/signal" + "time" + + "github.com/NorskHelsenett/ror-agent/internal/clients/clients" + "github.com/NorskHelsenett/ror-agent/internal/config" + "github.com/NorskHelsenett/ror-agent/internal/controllers" + "github.com/NorskHelsenett/ror-agent/internal/httpserver" + "github.com/NorskHelsenett/ror-agent/internal/scheduler" + "github.com/NorskHelsenett/ror-agent/internal/services" + "github.com/NorskHelsenett/ror-agent/internal/services/resourceupdatev2" + + "github.com/NorskHelsenett/ror-agent/internal/checks/initialchecks" + "github.com/NorskHelsenett/ror-agent/internal/kubernetes/operator/initialize" + + "github.com/NorskHelsenett/ror/pkg/config/configconsts" + + "github.com/NorskHelsenett/ror/pkg/rlog" + + "syscall" + + "github.com/spf13/viper" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/discovery" + + // https://blog.devgenius.io/know-gomaxprocs-before-deploying-your-go-app-to-kubernetes-7a458fb63af1 + + "go.uber.org/automaxprocs/maxprocs" +) + +// init +func init() { + _, _ = maxprocs.Set(maxprocs.Logger(rlog.Infof)) + config.Init() +} + +func main() { + _ = "rebuild 6" + rlog.Info("Agent is starting", rlog.String("version", viper.GetString(configconsts.VERSION))) + sigs := make(chan os.Signal, 1) // Create channel to receive os signals + stop := make(chan struct{}) // Create channel to receive stop signal + signal.Notify(sigs, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) // Register the sigs channel to receieve SIGTERM + + go func() { + services.GetEgressIp() + sig := <-sigs + _, _ = fmt.Println() + _, _ = fmt.Print(sig) + stop <- struct{}{} + }() + + clients.Initialize() + + k8sClient, err := clients.Kubernetes.GetKubernetesClientset() + if err != nil { + panic(err.Error()) + } + + discoveryClient, err := clients.Kubernetes.GetDiscoveryClient() + if err != nil { + rlog.Error("failed to get discovery client", err) + } + + dynamicClient, err := clients.Kubernetes.GetDynamicClient() + if err != nil { + rlog.Error("failed to get dynamic client", err) + } + + ns := viper.GetString(configconsts.POD_NAMESPACE) + if ns == "" { + rlog.Fatal("POD_NAMESPACE is not set", nil) + } + + _, err = k8sClient.CoreV1().Namespaces().Get(context.Background(), ns, metav1.GetOptions{}) + if err != nil { + rlog.Fatal("could not get namespace", err) + } + + err = initialchecks.HasSuccessfullRorApiConnection() + if err != nil { + rlog.Fatal("could not connect to ror-api", err) + } + + err = services.ExtractApikeyOrDie() + if err != nil { + rlog.Fatal("could not get or create secret", err) + } + + clusterId, err := initialize.GetOwnClusterId() + if err != nil { + rlog.Fatal("could not fetch clusterid from ror-api", err) + } + viper.Set(configconsts.CLUSTER_ID, clusterId) + + err = resourceupdatev2.ResourceCache.Init() + if err != nil { + rlog.Fatal("could not get hashlist for clusterid", err) + } + + err = scheduler.HeartbeatReporting() + if err != nil { + rlog.Fatal("could not send heartbeat to api", err) + } + + // waiting for ip check to finish :) + time.Sleep(time.Second * 1) + + schemas := clients.InitSchema() + + for _, schema := range schemas { + check, err := discovery.IsResourceEnabled(discoveryClient, schema) + if err != nil { + rlog.Error("Could not query resources from cluster", err) + } + if check { + controller := controllers.NewDynamicController(dynamicClient, schema) + + go func() { + controller.Run(stop) + sig := <-sigs + _, _ = fmt.Println() + _, _ = fmt.Println(sig) + stop <- struct{}{} + }() + } else { + errmsg := fmt.Sprintf("Could not register resource %s", schema.Resource) + rlog.Info(errmsg) + } + } + + go func() { + httpserver.InitHttpServer() + sig := <-sigs + _, _ = fmt.Println() + _, _ = fmt.Println(sig) + stop <- struct{}{} + }() + + scheduler.SetUpScheduler() + + <-stop + rlog.Info("Shutting down...") +} diff --git a/cmd/agent/version.json b/cmd/agent/version.json new file mode 100644 index 0000000..e1d46e1 --- /dev/null +++ b/cmd/agent/version.json @@ -0,0 +1,6 @@ +{ + "major": 1, + "minor": 1, + "patch": 372, + "commitSha": "ecafe0db" +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8a3156f --- /dev/null +++ b/go.mod @@ -0,0 +1,104 @@ +module github.com/NorskHelsenett/ror-agent + +go 1.23.1 + +require ( + github.com/NorskHelsenett/ror v0.3.11-rc3 + github.com/evanphx/json-patch/v5 v5.9.0 + github.com/go-co-op/gocron v1.37.0 + github.com/go-resty/resty/v2 v2.16.0 + github.com/google/go-cmp v0.6.0 + github.com/spf13/viper v1.19.0 + github.com/stretchr/testify v1.9.0 + go.uber.org/automaxprocs v1.6.0 + gopkg.in/yaml.v3 v3.0.1 + k8s.io/api v0.31.2 + k8s.io/apimachinery v0.31.2 + k8s.io/client-go v0.31.2 + k8s.io/metrics v0.31.2 + k8s.io/utils v0.0.0-20241104163129-6fe5fd82f078 + sigs.k8s.io/controller-runtime v0.19.1 +) + +require ( + github.com/bytedance/sonic v1.12.4 // indirect + github.com/bytedance/sonic/loader v0.2.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.6 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gin-gonic/gin v1.10.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.4 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.23.0 // indirect + github.com/goccy/go-json v0.10.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/imdario/mergo v0.3.6 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.mongodb.org/mongo-driver v1.17.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect + go.opentelemetry.io/otel v1.32.0 // indirect + go.opentelemetry.io/otel/metric v1.32.0 // indirect + go.opentelemetry.io/otel/trace v1.32.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/arch v0.12.0 // indirect + golang.org/x/crypto v0.29.0 // indirect + golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect + golang.org/x/net v0.31.0 // indirect + golang.org/x/oauth2 v0.23.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/term v0.26.0 // indirect + golang.org/x/text v0.20.0 // indirect + golang.org/x/time v0.6.0 // indirect + google.golang.org/protobuf v1.35.2 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c3ddf35 --- /dev/null +++ b/go.sum @@ -0,0 +1,286 @@ +github.com/NorskHelsenett/ror v0.3.11-rc3 h1:IoP5CYIupD4HryeGUOjmJArYuOR7MfYavER4jkdVhQk= +github.com/NorskHelsenett/ror v0.3.11-rc3/go.mod h1:k3YOVf4W/DqJ/fKNSv4+m9H8ylTlQhAEyoU59bzpDP0= +github.com/bytedance/sonic v1.12.4 h1:9Csb3c9ZJhfUWeMtpCDCq6BUoH5ogfDFLUgQ/jG+R0k= +github.com/bytedance/sonic v1.12.4/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E= +github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= +github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc= +github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0= +github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= +github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-resty/resty/v2 v2.16.0 h1:qpKalHWI2bpp9BIKlyT8TYWEJXOk1NuKbfiT3RRnzWc= +github.com/go-resty/resty/v2 v2.16.0/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM= +go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI= +go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= +go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= +go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= +go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= +go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= +go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg= +golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= +golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.31.2 h1:3wLBbL5Uom/8Zy98GRPXpJ254nEFpl+hwndmk9RwmL0= +k8s.io/api v0.31.2/go.mod h1:bWmGvrGPssSK1ljmLzd3pwCQ9MgoTsRCuK35u6SygUk= +k8s.io/apimachinery v0.31.2 h1:i4vUt2hPK56W6mlT7Ry+AO8eEsyxMD1U44NR22CLTYw= +k8s.io/apimachinery v0.31.2/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/client-go v0.31.2 h1:Y2F4dxU5d3AQj+ybwSMqQnpZH9F30//1ObxOKlTI9yc= +k8s.io/client-go v0.31.2/go.mod h1:NPa74jSVR/+eez2dFsEIHNa+3o09vtNaWwWwb1qSxSs= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= +k8s.io/metrics v0.31.2 h1:sQhujR9m3HN/Nu/0fTfTscjnswQl0qkQAodEdGBS0N4= +k8s.io/metrics v0.31.2/go.mod h1:QqqyReApEWO1UEgXOSXiHCQod6yTxYctbAAQBWZkboU= +k8s.io/utils v0.0.0-20241104163129-6fe5fd82f078 h1:jGnCPejIetjiy2gqaJ5V0NLwTpF4wbQ6cZIItJCSHno= +k8s.io/utils v0.0.0-20241104163129-6fe5fd82f078/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +sigs.k8s.io/controller-runtime v0.19.1 h1:Son+Q40+Be3QWb+niBXAg2vFiYWolDjjRfO8hn/cxOk= +sigs.k8s.io/controller-runtime v0.19.1/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/internal/checks/initialchecks/initial_checks.go b/internal/checks/initialchecks/initial_checks.go new file mode 100644 index 0000000..0f8e753 --- /dev/null +++ b/internal/checks/initialchecks/initial_checks.go @@ -0,0 +1,46 @@ +// TODO: This internal package is copied from ror, should determine if its common and should be moved to ror/pkg +package initialchecks + +import ( + "fmt" + + "github.com/NorskHelsenett/ror-agent/internal/clients/rorapiclient" + + "github.com/NorskHelsenett/ror/pkg/config/configconsts" + + "github.com/NorskHelsenett/ror/pkg/rlog" + + "github.com/spf13/viper" +) + +// HasSuccessfullRorApiConnection checks whether there is a successful connection to the ROR API. +// +// - If the ROR_URL is not set, it returns an error. +// Returns: +// - (bool): True if the connection to the ROR API is successful, false otherwise. +func HasSuccessfullRorApiConnection() error { + if viper.GetString(configconsts.API_ENDPOINT) == "" { + return fmt.Errorf("ROR_URL is not set") + } + + rorClient, err := rorapiclient.GetOrCreateRorRestyClientNonAuth() + if err != nil { + rlog.Error("could not get ror-api client", err) + return err + } + + url := "/v1/info/version" + response, err := rorClient.R(). + SetHeader("Content-Type", "application/json"). + Get(url) + if err != nil { + rlog.Error("could not get data from ror-api", err) + return err + } + + if response.StatusCode() > 299 { + return fmt.Errorf("unsuccessful connection to ror-api: status code %d", response.StatusCode()) + } + + return nil +} diff --git a/internal/clients/clients/dynamicclient.go b/internal/clients/clients/dynamicclient.go new file mode 100644 index 0000000..3757c43 --- /dev/null +++ b/internal/clients/clients/dynamicclient.go @@ -0,0 +1,10 @@ +package clients + +import ( + "github.com/NorskHelsenett/ror/pkg/rorresources/rordefs" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func InitSchema() []schema.GroupVersionResource { + return rordefs.GetSchemasByType(rordefs.ApiResourceTypeAgent) +} diff --git a/internal/clients/clients/kubernetes.go b/internal/clients/clients/kubernetes.go new file mode 100644 index 0000000..9689514 --- /dev/null +++ b/internal/clients/clients/kubernetes.go @@ -0,0 +1,11 @@ +package clients + +import ( + kubernetesclient "github.com/NorskHelsenett/ror/pkg/clients/kubernetes" +) + +var Kubernetes *kubernetesclient.K8sClientsets + +func Initialize() { + Kubernetes = kubernetesclient.NewK8sClientConfig() +} diff --git a/internal/clients/clients/ror.go b/internal/clients/clients/ror.go new file mode 100644 index 0000000..d4b1720 --- /dev/null +++ b/internal/clients/clients/ror.go @@ -0,0 +1,39 @@ +// The package implements clients for the ror-agent +package clients + +import ( + "fmt" + + "github.com/NorskHelsenett/ror/pkg/config/configconsts" + + "github.com/go-resty/resty/v2" + "github.com/spf13/viper" +) + +var client *resty.Client +var clientNonAuth *resty.Client + +func GetOrCreateRorClient() (*resty.Client, error) { + if client != nil { + return client, nil + } + + client = resty.New() + client.SetBaseURL(viper.GetString(configconsts.API_ENDPOINT)) + client.Header.Add("X-API-KEY", viper.GetString(configconsts.API_KEY)) + client.Header.Set("User-Agent", fmt.Sprintf("ROR-Agent/%s", viper.GetString(configconsts.VERSION))) + + return client, nil +} + +func GetOrCreateRorClientNonAuth() (*resty.Client, error) { + if clientNonAuth != nil { + return clientNonAuth, nil + } + + clientNonAuth = resty.New() + clientNonAuth.SetBaseURL(viper.GetString(configconsts.API_ENDPOINT)) + clientNonAuth.Header.Set("User-Agent", fmt.Sprintf("ROR-Agent/%s", viper.GetString(configconsts.VERSION))) + + return clientNonAuth, nil +} diff --git a/internal/clients/rorapiclient/old.go b/internal/clients/rorapiclient/old.go new file mode 100644 index 0000000..01addff --- /dev/null +++ b/internal/clients/rorapiclient/old.go @@ -0,0 +1,40 @@ +// TODO: This internal package is copied from ror, should determine if its common and should be moved to ror/pkg +package rorapiclient + +import ( + "fmt" + + "github.com/NorskHelsenett/ror/pkg/config/configconsts" + + "github.com/go-resty/resty/v2" + "github.com/spf13/viper" +) + +var rorclient *resty.Client +var rorclientnonauth *resty.Client + +// Deprecated: GetOrCreateRorRestyClient is deprecated. Use rorclient instead +func GetOrCreateRorRestyClient() (*resty.Client, error) { + if rorclient != nil { + return rorclient, nil + } + + rorclient = resty.New() + rorclient.SetBaseURL(viper.GetString(configconsts.API_ENDPOINT)) + rorclient.Header.Add("X-API-KEY", viper.GetString(configconsts.API_KEY)) + rorclient.Header.Set("User-Agent", fmt.Sprintf("ROR-Agent/%s", viper.GetString(configconsts.VERSION))) + + return rorclient, nil +} + +// Deprecated: GetOrCreateRorRestyClientNonAuth is deprecated. Use rorclient instead +func GetOrCreateRorRestyClientNonAuth() (*resty.Client, error) { + if rorclientnonauth != nil { + return rorclientnonauth, nil + } + + rorclientnonauth = resty.New() + rorclientnonauth.SetBaseURL(viper.GetString(configconsts.API_ENDPOINT)) + rorclientnonauth.Header.Set("User-Agent", fmt.Sprintf("ROR-Agent/%s", viper.GetString(configconsts.VERSION))) + return rorclientnonauth, nil +} diff --git a/internal/config/variables.go b/internal/config/variables.go new file mode 100644 index 0000000..3027293 --- /dev/null +++ b/internal/config/variables.go @@ -0,0 +1,34 @@ +package config + +import ( + "github.com/NorskHelsenett/ror/pkg/config/configconsts" + + "github.com/NorskHelsenett/ror/pkg/rlog" + + "github.com/spf13/viper" +) + +var ( + version string = "1.1.0" + commit string = "FFFFF" + ErrorCount int +) + +func Init() { + rlog.InitializeRlog() + rlog.Info("Configuration initializing ...") + viper.SetDefault(configconsts.VERSION, version) + viper.SetDefault(configconsts.COMMIT, commit) + viper.SetDefault(configconsts.HEALTH_ENDPOINT, ":8100") + viper.SetDefault(configconsts.POD_NAMESPACE, "ror") + viper.SetDefault(configconsts.API_KEY_SECRET, "ror-apikey") + + viper.AutomaticEnv() +} + +func IncreaseErrorCount() { + ErrorCount++ +} +func ResetErrorCount() { + ErrorCount = 0 +} diff --git a/internal/controllers/dynamicController.go b/internal/controllers/dynamicController.go new file mode 100644 index 0000000..6dd3081 --- /dev/null +++ b/internal/controllers/dynamicController.go @@ -0,0 +1,60 @@ +package controllers + +import ( + "github.com/NorskHelsenett/ror-agent/internal/services/resourceupdatev2" + + "github.com/NorskHelsenett/ror/pkg/apicontracts/apiresourcecontracts" + "github.com/NorskHelsenett/ror/pkg/rlog" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/dynamic/dynamicinformer" + "k8s.io/client-go/tools/cache" +) + +type DynamicController struct { + dynInformer cache.SharedIndexInformer + client dynamic.Interface +} + +func (c *DynamicController) Run(stop <-chan struct{}) { + // Execute go function + go c.dynInformer.Run(stop) +} + +// Function creates a new dynamic controller to listen for api-changes in provided GroupVersionResource +func NewDynamicController(client dynamic.Interface, resource schema.GroupVersionResource) *DynamicController { + dynWatcher := &DynamicController{} + dynInformer := dynamicinformer.NewDynamicSharedInformerFactory(client, 0) + informer := dynInformer.ForResource(resource).Informer() + + dynWatcher.client = client + dynWatcher.dynInformer = informer + + _, err := informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: addResource, + UpdateFunc: updateResource, + DeleteFunc: deleteResource, + }) + + if err != nil { + rlog.Error("Error adding event handler", err) + } + return dynWatcher +} + +func addResource(obj any) { + rawData := obj.(*unstructured.Unstructured) + resourceupdatev2.SendResource(apiresourcecontracts.K8sActionAdd, rawData) +} + +func deleteResource(obj any) { + rawData := obj.(*unstructured.Unstructured) + resourceupdatev2.SendResource(apiresourcecontracts.K8sActionDelete, rawData) +} + +func updateResource(_ any, obj any) { + rawData := obj.(*unstructured.Unstructured) + resourceupdatev2.SendResource(apiresourcecontracts.K8sActionUpdate, rawData) +} diff --git a/internal/httpserver/httpServerModels.go b/internal/httpserver/httpServerModels.go new file mode 100644 index 0000000..a56a268 --- /dev/null +++ b/internal/httpserver/httpServerModels.go @@ -0,0 +1,20 @@ +package httpserver + +type HealthStatus struct { + Status string `json:"status"` + Report HealthReport `json:"report"` +} + +type HealthReport struct { + Kubernetes K8sReport `json:"kubernetes"` + RorApi RorApiReport `json:"rorApi"` + ErrorCount int `json:"errorCount"` +} + +type K8sReport struct { + HasConfig bool `json:"hasConfig"` +} + +type RorApiReport struct { + GotToken bool `json:"token"` +} diff --git a/internal/httpserver/server.go b/internal/httpserver/server.go new file mode 100644 index 0000000..b3324d7 --- /dev/null +++ b/internal/httpserver/server.go @@ -0,0 +1,79 @@ +package httpserver + +import ( + "encoding/json" + "net/http" + "strings" + + "github.com/NorskHelsenett/ror-agent/internal/clients/clients" + "github.com/NorskHelsenett/ror-agent/internal/config" + + "github.com/NorskHelsenett/ror/pkg/config/configconsts" + "github.com/NorskHelsenett/ror/pkg/helpers/otel/httpserver" + "github.com/NorskHelsenett/ror/pkg/rlog" + + "github.com/spf13/viper" +) + +var healthStatus HealthStatus + +func InitHttpServer() { + serveAddress := viper.GetString(configconsts.HEALTH_ENDPOINT) + + healthStatus = getHealthReportOrDie() + + err := httpserver.RunOtelHttpHealthServer(serveAddress, health) + if err != nil { + rlog.Fatal("could not start health server", err) + } +} + +func health(w http.ResponseWriter, req *http.Request) { + healthStatus = getHealthReportOrDie() + js, err := json.Marshal(healthStatus) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + if strings.Contains(healthStatus.Status, "Crap") || strings.Contains(healthStatus.Status, "UnHealthy") || healthStatus.Report.ErrorCount > 0 || len(viper.GetString(configconsts.API_KEY)) == 0 { + w.WriteHeader(500) + } + + _, _ = w.Write(js) +} + +func getHealthReportOrDie() (result HealthStatus) { + gotAuthSetting := false + if len(viper.GetString(configconsts.API_KEY)) != 0 { + gotAuthSetting = true + } + + hasK8sConfig := false + if clients.Kubernetes.GetConfig() != nil { + hasK8sConfig = true + } + + status := "Crap ... ET phone home!" + if hasK8sConfig && config.ErrorCount == 0 && gotAuthSetting { + status = "Healthy" + } else if hasK8sConfig && (!gotAuthSetting || config.ErrorCount > 0) { + status = "UnHealthy" + } + + healthStatus = HealthStatus{ + Status: status, + Report: HealthReport{ + Kubernetes: K8sReport{ + HasConfig: hasK8sConfig, + }, + RorApi: RorApiReport{ + GotToken: gotAuthSetting, + }, + ErrorCount: config.ErrorCount, + }, + } + + return healthStatus +} diff --git a/internal/kubernetes/k8smodels/k8s_models.go b/internal/kubernetes/k8smodels/k8s_models.go new file mode 100644 index 0000000..0189148 --- /dev/null +++ b/internal/kubernetes/k8smodels/k8s_models.go @@ -0,0 +1,36 @@ +// TODO: This internal package is copied from ror, should determine if its common and should be moved to ror/pkg +package k8smodels + +import ( + "time" + + "github.com/NorskHelsenett/ror/pkg/apicontracts" + "github.com/NorskHelsenett/ror/pkg/models/providers" +) + +type Node struct { + Name string `json:"name"` + Created time.Time `json:"created"` + OsImage string `json:"osImage"` + ClusterName string `json:"clusterName"` + Workspace string `json:"workspace"` + Datacenter string `json:"datacenter"` + MachineName string `json:"machineName"` + Labels map[string]string `json:"labels"` + Annotations map[string]string `json:"annotations"` + Resources apicontracts.NodeResources `json:"resources"` + Architecture string `json:"architecture"` + ContainerRuntimeVersion string `json:"containerRuntimeVersion"` + KernelVersion string `json:"kernelVersion"` + KubeProxyVersion string `json:"kubeProxyVersion"` + KubeletVersion string `json:"kubeletVersion"` + OperatingSystem string `json:"operatingSystem"` + Provider providers.ProviderType `json:"provider"` +} + +type NhnTooling struct { + Version string `json:"version"` + Branch string `json:"branch"` + Environment string `json:"environment"` + AccessGroups []string `json:"accessGroups"` +} diff --git a/internal/kubernetes/nodeservice/node_service.go b/internal/kubernetes/nodeservice/node_service.go new file mode 100644 index 0000000..5214800 --- /dev/null +++ b/internal/kubernetes/nodeservice/node_service.go @@ -0,0 +1,134 @@ +// TODO: This internal package is copied from ror, should determine if its common and should be moved to ror/pkg +package nodeservice + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/NorskHelsenett/ror-agent/internal/kubernetes/k8smodels" + + "github.com/NorskHelsenett/ror/pkg/apicontracts" + "github.com/NorskHelsenett/ror/pkg/kubernetes/interregators/providerinterregationreport" + "github.com/NorskHelsenett/ror/pkg/models/providers" + + "github.com/NorskHelsenett/ror/pkg/rlog" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + metrics "k8s.io/metrics/pkg/client/clientset/versioned" +) + +func GetNodes(k8sClient *kubernetes.Clientset, metricsClient *metrics.Clientset) ([]k8smodels.Node, error) { + var nodes []k8smodels.Node + list, err := k8sClient.CoreV1().Nodes().List(context.TODO(), v1.ListOptions{}) + if err != nil { + rlog.Error("could not get node list", err) + return nil, errors.New("could not extract node list ") + } + report, err := providerinterregationreport.NewInterregationReport(list.Items) + if err != nil { + rlog.Error("could not get provider type", err) + } + rlog.Debug(fmt.Sprintf("Provider detected: %s", report.GetProvider())) + + for _, node := range list.Items { + + n := k8smodels.Node{} + + n.Workspace = report.Workspace + n.ClusterName = report.ClusterName + n.Datacenter = report.Datacenter + n.Provider = report.Provider + + n.OsImage = node.Status.NodeInfo.OSImage + n.Created = node.CreationTimestamp.Time + n.Annotations = node.Annotations + n.Name = node.Name + n.Labels = node.Labels + + switch report.GetProvider() { + case providers.ProviderTypeTanzu: + fillNodeTanzu(&n) + case providers.ProviderTypeAks: + fillNodeAzure(&n) + case providers.ProviderTypeK3d: + fillNodeK3d(&n) + case providers.ProviderTypeKind: + fillNodeKind(&n) + case providers.ProviderTypeGke: + fillNodeGke(&n) + case providers.ProviderTypeTalos: + fillNodeTalos(&n) + default: + fillNodeDefault(&n) + } + + n.Architecture = node.Status.NodeInfo.Architecture + n.ContainerRuntimeVersion = node.Status.NodeInfo.ContainerRuntimeVersion + n.KernelVersion = node.Status.NodeInfo.KernelVersion + n.KubeProxyVersion = node.Status.NodeInfo.KubeProxyVersion + n.KubeletVersion = node.Status.NodeInfo.KubeletVersion + n.OperatingSystem = node.Status.NodeInfo.OperatingSystem + + nodeMetrics, err := metricsClient.MetricsV1beta1().NodeMetricses().Get(context.TODO(), node.Name, v1.GetOptions{}) + if err == nil { + cpuUsage := nodeMetrics.Usage.Cpu() + cpuAllocated, _ := node.Status.Allocatable.Cpu().AsInt64() + + memoryUsageInt, _ := nodeMetrics.Usage.Memory().AsInt64() + memoryAllocated := node.Status.Allocatable.Memory().Value() + + n.Resources = apicontracts.NodeResources{ + Allocated: apicontracts.ResourceAllocated{ + Cpu: cpuAllocated, + MemoryBytes: memoryAllocated, + }, + Consumed: apicontracts.ResourceConsumed{ + CpuMilliValue: cpuUsage.MilliValue(), + MemoryBytes: memoryUsageInt, + }, + } + } else { + rlog.Debug("could not fetch node metrics", rlog.String("name", node.Name)) + } + + nodes = append(nodes, n) + } + + return nodes, nil +} + +func fillNodeTanzu(n *k8smodels.Node) { + n.MachineName = n.Labels["kubernetes.io/hostname"] + workspaceArray := strings.Split(n.Workspace, "-") + if len(workspaceArray) > 0 { + n.Datacenter = workspaceArray[0] + } +} + +func fillNodeAzure(n *k8smodels.Node) { + n.MachineName = n.Labels["kubernetes.io/hostname"] +} + +func fillNodeK3d(n *k8smodels.Node) { + hostname := n.Labels["kubernetes.io/hostname"] + + n.MachineName = fmt.Sprintf("%s-%s", hostname, "localhost") +} +func fillNodeKind(n *k8smodels.Node) { + hostname := n.Labels["kubernetes.io/hostname"] + n.MachineName = fmt.Sprintf("%s-%s", hostname, "localhost") +} +func fillNodeGke(n *k8smodels.Node) { + n.MachineName = n.Labels["kubernetes.io/hostname"] +} + +func fillNodeTalos(n *k8smodels.Node) { + n.MachineName = n.Labels["kubernetes.io/hostname"] +} + +func fillNodeDefault(n *k8smodels.Node) { + n.MachineName = "Unknown" +} diff --git a/internal/kubernetes/operator/initialize/initialize_operator.go b/internal/kubernetes/operator/initialize/initialize_operator.go new file mode 100644 index 0000000..c6ac56e --- /dev/null +++ b/internal/kubernetes/operator/initialize/initialize_operator.go @@ -0,0 +1,144 @@ +// TODO: This internal package is copied from ror, should determine if its common and should be moved to ror/pkg +package initialize + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "time" + + "github.com/NorskHelsenett/ror-agent/internal/kubernetes/nodeservice" + "github.com/NorskHelsenett/ror-agent/internal/models/operatormodels" + + "github.com/NorskHelsenett/ror/pkg/config/configconsts" + + "github.com/NorskHelsenett/ror/pkg/apicontracts" + + "github.com/NorskHelsenett/ror/pkg/rlog" + + "github.com/spf13/viper" + "k8s.io/client-go/kubernetes" + metrics "k8s.io/metrics/pkg/client/clientset/versioned" +) + +// Deprecated: GetApikey is deprecated use something like cmd\agentv2\clients\rorconfig.go instead +func GetApikey(clusterInfo *operatormodels.ClusterInfo, rorUrl string) (string, error) { + if clusterInfo == nil { + return "", errors.New("identifier is empty, cannot fetch api key") + } + + client := http.Client{Timeout: time.Duration(20) * time.Second} + jsonData, _ := json.Marshal(apicontracts.AgentApiKeyModel{ + Identifier: clusterInfo.ClusterName, + DatacenterName: clusterInfo.DatacenterName, + WorkspaceName: clusterInfo.WorkspaceName, + Provider: clusterInfo.Provider, + Type: "Cluster", + }) + + requestUrl := fmt.Sprintf("%s/v1/clusters/register", rorUrl) + response, err := client.Post(requestUrl, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + rlog.Error("error when getting api key", err) + return "", errors.New("could not send data to API") + } + + if response.StatusCode > 299 { + bodyByte, err := io.ReadAll(response.Body) + if err != nil { + rlog.Fatal("Response body", err, rlog.ByteString("body", bodyByte)) + } + rlog.Error("could not get api key from API", fmt.Errorf("non 200 response code"), + rlog.String("identifier", clusterInfo.ClusterName), + rlog.Int("response code", response.StatusCode), + rlog.String("url", rorUrl)) + + return "", fmt.Errorf("could not get api key from API, identifier: %s, rorUrl: %s", clusterInfo.ClusterName, rorUrl) + } + + body := response.Body + bodyByte, err := io.ReadAll(body) + if err != nil { + rlog.Error("could not read body", err) + return "", errors.New("could not read data from response") + } + + apikey := string(bodyByte) + return apikey, nil +} + +// Deprecated: GetClusterInfoFromNode is deprecated use kubernetes interegator instead +func GetClusterInfoFromNode(k8sClient *kubernetes.Clientset, metricsClient *metrics.Clientset) (*operatormodels.ClusterInfo, error) { + nodes, err := nodeservice.GetNodes(k8sClient, metricsClient) + if err != nil { + rlog.Error("could not get nodes", err) + return nil, errors.New("could not get clusterId") + } + + if len(nodes) < 1 { + rlog.Error("nodes list is empty", nil) + return nil, errors.New("nodes list is empty") + } + + firstNode := nodes[0] + clusterName := firstNode.ClusterName + workspaceName := firstNode.Workspace + + clusterId := fmt.Sprintf("%s.%s", clusterName, workspaceName) + clusterInfo := &operatormodels.ClusterInfo{ + Id: clusterId, + ClusterName: clusterName, + DatacenterName: firstNode.Datacenter, + WorkspaceName: firstNode.Workspace, + Provider: firstNode.Provider, + } + + return clusterInfo, nil +} + +func GetOwnClusterId() (string, error) { + apikey := viper.GetString(configconsts.API_KEY) + rorUrl := viper.GetString(configconsts.API_ENDPOINT) + + client := http.Client{Timeout: time.Duration(10) * time.Second} + request, err := http.NewRequest("GET", rorUrl+"/v1/clusters/self", nil) + if err != nil { + return "", err + } + request.Header.Set("Content-Type", "application/json") + request.Header.Set("X-API-KEY", apikey) + + response, err := client.Do(request) + if err != nil { + rlog.Error("error when cluster self info", err) + return "", errors.New("could not get data from ROR-API") + } + + if response.StatusCode > 299 { + bodyByte, err := io.ReadAll(response.Body) + if err != nil { + rlog.Fatal("response body: ", err, rlog.ByteString("bytes", bodyByte)) + } + + return "", fmt.Errorf("could not get cluster self data from API, rorUrl: %s", rorUrl) + } + + body := response.Body + bodyByte, err := io.ReadAll(body) + if err != nil { + rlog.Error("could not read body", err) + return "", errors.New("could not read data from response") + } + + var clusterSelf apicontracts.ClusterSelf + err = json.Unmarshal(bodyByte, &clusterSelf) + if err != nil { + rlog.Error("could not unmarshal body", err) + rlog.Fatal("Could not fetch secret", err) + } + + return clusterSelf.ClusterId, nil +} diff --git a/internal/models/argomodels/argo.go b/internal/models/argomodels/argo.go new file mode 100644 index 0000000..c78d6cd --- /dev/null +++ b/internal/models/argomodels/argo.go @@ -0,0 +1,83 @@ +package argomodels + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type Applications struct { + Items []Application `json:"items"` +} + +type Application struct { + ApiVersion string `json:"apiVersion"` + Kind string `json:"kind"` + Metadata metav1.ObjectMeta `json:"metadata"` + //Operation ApplicationOperation `json:"operation"` + Spec ApplicationSpec `json:"spec"` + Status ApplicationStatus `json:"status"` +} + +type ApplicationSpec struct { + Destination ApplicationDestination `json:"destination"` + IgnoredDifferences []string `json:"ignoredDifferences"` + Info []ApplicationInfo `json:"info"` + Source ApplicationSource `json:"source"` +} + +type ApplicationDestination struct { + Name string `json:"name"` + Namespace string `json:"namespace"` + Server string `json:"server"` +} + +type ApplicationInfo struct { + Name string `json:"name"` + Value string `json:"value"` +} + +type ApplicationSource struct { + Chart string `json:"chart"` + Directory ApplicationSourceDirectory `json:"directory"` + Helm ApplicationSourceHelm `json:"helm"` + Path string `json:"path"` + Ref string `json:"ref"` + RepoUrl string `json:"repoUrl"` + TargetRevision string `json:"targetRevision"` +} + +type ApplicationSourceDirectory struct { + Exclude string `json:"exclude"` + Include string `json:"include"` + Recurse bool `json:"recurse"` + //Jsonnet ApplicationSourceDirectoryJsonnet `json:"jsonnet"` +} + +type ApplicationSourceHelm struct { +} + +type ApplicationStatus struct { + //Conditions []ApplicationStatusCondition `json:"conditions"` + ControllerNamespace string `json:"controllerNamespace"` + //Health ApplicationStatusHealth `json:"health"` + //History []ApplicationStatusHistory `json:"history"` + ObservedAt string `json:"observedAt"` + //OperationState ApplicationStatusOperationState `json:"operationState"` + ReconciledAt string `json:"reconciledAt"` + ResourceHealthSource string `json:"resourceHealthSource"` + //Resources []ApplicationStatusResource `json:"resources"` + SourceType string `json:"sourceType"` + Summary ApplicationStatusSummary `json:"summary"` + Sync ApplicationStatusSync `json:"sync"` +} + +type ApplicationStatusSummary struct { + ExternalUrls []string `json:"externalUrls"` + Images []string `json:"images"` +} + +type ApplicationStatusSync struct { + //ComparedTo ApplicationStatusSyncComparedTo `json:"comparedTo"` + Revision string `json:"revision"` + Revisions []string `json:"revisions"` + Status string `json:"status"` +} diff --git a/internal/models/operatormodels/models.go b/internal/models/operatormodels/models.go new file mode 100644 index 0000000..5b6fbbc --- /dev/null +++ b/internal/models/operatormodels/models.go @@ -0,0 +1,11 @@ +package operatormodels + +import "github.com/NorskHelsenett/ror/pkg/models/providers" + +type ClusterInfo struct { + Id string `json:"id"` + ClusterName string `json:"clusterName"` + DatacenterName string `json:"datacenterName"` + WorkspaceName string `json:"workspaceName"` + Provider providers.ProviderType `json:"provider"` +} diff --git a/internal/models/rorResources/docs.go b/internal/models/rorResources/docs.go new file mode 100644 index 0000000..7566b89 --- /dev/null +++ b/internal/models/rorResources/docs.go @@ -0,0 +1 @@ +package rorResources diff --git a/internal/models/rorResources/extractResource.go b/internal/models/rorResources/extractResource.go new file mode 100644 index 0000000..9ca630c --- /dev/null +++ b/internal/models/rorResources/extractResource.go @@ -0,0 +1,219 @@ +// THIS FILE IS GENERATED, DO NOT EDIT +// ref: build/generator/main.go +package rorResources + +import ( + "fmt" + apiresourcecontracts "github.com/NorskHelsenett/ror/pkg/apicontracts/apiresourcecontracts" +) + +// the function determines which model to match the resource to and call prepareResourcePayload to cast the input to the matching model. +func (rj rorResourceJson) getResource(resourceReturn *rorResource) error { + bytes := []byte(rj) + + if resourceReturn.ApiVersion == "v1" && resourceReturn.Kind == "Namespace" { + payload, err := prepareResourcePayload[apiresourcecontracts.ResourceNamespace](bytes) + resourceReturn.Resource = payload + return err + } + + if resourceReturn.ApiVersion == "v1" && resourceReturn.Kind == "Node" { + payload, err := prepareResourcePayload[apiresourcecontracts.ResourceNode](bytes) + resourceReturn.Resource = payload + return err + } + + if resourceReturn.ApiVersion == "v1" && resourceReturn.Kind == "PersistentVolumeClaim" { + payload, err := prepareResourcePayload[apiresourcecontracts.ResourcePersistentVolumeClaim](bytes) + resourceReturn.Resource = payload + return err + } + + if resourceReturn.ApiVersion == "apps/v1" && resourceReturn.Kind == "Deployment" { + payload, err := prepareResourcePayload[apiresourcecontracts.ResourceDeployment](bytes) + resourceReturn.Resource = payload + return err + } + + if resourceReturn.ApiVersion == "storage.k8s.io/v1" && resourceReturn.Kind == "StorageClass" { + payload, err := prepareResourcePayload[apiresourcecontracts.ResourceStorageClass](bytes) + resourceReturn.Resource = payload + return err + } + + if resourceReturn.ApiVersion == "wgpolicyk8s.io/v1alpha2" && resourceReturn.Kind == "PolicyReport" { + payload, err := prepareResourcePayload[apiresourcecontracts.ResourcePolicyReport](bytes) + resourceReturn.Resource = payload + return err + } + + if resourceReturn.ApiVersion == "argoproj.io/v1alpha1" && resourceReturn.Kind == "Application" { + payload, err := prepareResourcePayload[apiresourcecontracts.ResourceApplication](bytes) + resourceReturn.Resource = payload + return err + } + + if resourceReturn.ApiVersion == "argoproj.io/v1alpha1" && resourceReturn.Kind == "AppProject" { + payload, err := prepareResourcePayload[apiresourcecontracts.ResourceAppProject](bytes) + resourceReturn.Resource = payload + return err + } + + if resourceReturn.ApiVersion == "cert-manager.io/v1" && resourceReturn.Kind == "Certificate" { + payload, err := prepareResourcePayload[apiresourcecontracts.ResourceCertificate](bytes) + resourceReturn.Resource = payload + return err + } + + if resourceReturn.ApiVersion == "v1" && resourceReturn.Kind == "Service" { + payload, err := prepareResourcePayload[apiresourcecontracts.ResourceService](bytes) + resourceReturn.Resource = payload + return err + } + + if resourceReturn.ApiVersion == "v1" && resourceReturn.Kind == "Pod" { + payload, err := prepareResourcePayload[apiresourcecontracts.ResourcePod](bytes) + resourceReturn.Resource = payload + return err + } + + if resourceReturn.ApiVersion == "apps/v1" && resourceReturn.Kind == "ReplicaSet" { + payload, err := prepareResourcePayload[apiresourcecontracts.ResourceReplicaSet](bytes) + resourceReturn.Resource = payload + return err + } + + if resourceReturn.ApiVersion == "apps/v1" && resourceReturn.Kind == "StatefulSet" { + payload, err := prepareResourcePayload[apiresourcecontracts.ResourceStatefulSet](bytes) + resourceReturn.Resource = payload + return err + } + + if resourceReturn.ApiVersion == "apps/v1" && resourceReturn.Kind == "DaemonSet" { + payload, err := prepareResourcePayload[apiresourcecontracts.ResourceDaemonSet](bytes) + resourceReturn.Resource = payload + return err + } + + if resourceReturn.ApiVersion == "networking.k8s.io/v1" && resourceReturn.Kind == "Ingress" { + payload, err := prepareResourcePayload[apiresourcecontracts.ResourceIngress](bytes) + resourceReturn.Resource = payload + return err + } + + if resourceReturn.ApiVersion == "networking.k8s.io/v1" && resourceReturn.Kind == "IngressClass" { + payload, err := prepareResourcePayload[apiresourcecontracts.ResourceIngressClass](bytes) + resourceReturn.Resource = payload + return err + } + + if resourceReturn.ApiVersion == "aquasecurity.github.io/v1alpha1" && resourceReturn.Kind == "VulnerabilityReport" { + payload, err := prepareResourcePayload[apiresourcecontracts.ResourceVulnerabilityReport](bytes) + resourceReturn.Resource = payload + return err + } + + if resourceReturn.ApiVersion == "aquasecurity.github.io/v1alpha1" && resourceReturn.Kind == "ExposedSecretReport" { + payload, err := prepareResourcePayload[apiresourcecontracts.ResourceExposedSecretReport](bytes) + resourceReturn.Resource = payload + return err + } + + if resourceReturn.ApiVersion == "aquasecurity.github.io/v1alpha1" && resourceReturn.Kind == "ConfigAuditReport" { + payload, err := prepareResourcePayload[apiresourcecontracts.ResourceConfigAuditReport](bytes) + resourceReturn.Resource = payload + return err + } + + if resourceReturn.ApiVersion == "aquasecurity.github.io/v1alpha1" && resourceReturn.Kind == "RbacAssessmentReport" { + payload, err := prepareResourcePayload[apiresourcecontracts.ResourceRbacAssessmentReport](bytes) + resourceReturn.Resource = payload + return err + } + + if resourceReturn.ApiVersion == "run.tanzu.vmware.com/v1alpha2" && resourceReturn.Kind == "TanzuKubernetesCluster" { + payload, err := prepareResourcePayload[apiresourcecontracts.ResourceTanzuKubernetesCluster](bytes) + resourceReturn.Resource = payload + return err + } + + if resourceReturn.ApiVersion == "run.tanzu.vmware.com/v1alpha2" && resourceReturn.Kind == "TanzuKubernetesRelease" { + payload, err := prepareResourcePayload[apiresourcecontracts.ResourceTanzuKubernetesRelease](bytes) + resourceReturn.Resource = payload + return err + } + + if resourceReturn.ApiVersion == "vmoperator.vmware.com/v1alpha1" && resourceReturn.Kind == "VirtualMachineClass" { + payload, err := prepareResourcePayload[apiresourcecontracts.ResourceVirtualMachineClass](bytes) + resourceReturn.Resource = payload + return err + } + + if resourceReturn.ApiVersion == "vmoperator.vmware.com/v1alpha1" && resourceReturn.Kind == "VirtualMachineClassBinding" { + payload, err := prepareResourcePayload[apiresourcecontracts.ResourceVirtualMachineClassBinding](bytes) + resourceReturn.Resource = payload + return err + } + + if resourceReturn.ApiVersion == "general.ror.internal/v1alpha1" && resourceReturn.Kind == "KubernetesCluster" { + payload, err := prepareResourcePayload[apiresourcecontracts.ResourceKubernetesCluster](bytes) + resourceReturn.Resource = payload + return err + } + + if resourceReturn.ApiVersion == "general.ror.internal/v1alpha1" && resourceReturn.Kind == "ClusterOrder" { + payload, err := prepareResourcePayload[apiresourcecontracts.ResourceClusterOrder](bytes) + resourceReturn.Resource = payload + return err + } + + if resourceReturn.ApiVersion == "general.ror.internal/v1alpha1" && resourceReturn.Kind == "Project" { + payload, err := prepareResourcePayload[apiresourcecontracts.ResourceProject](bytes) + resourceReturn.Resource = payload + return err + } + + if resourceReturn.ApiVersion == "general.ror.internal/v1alpha1" && resourceReturn.Kind == "Configuration" { + payload, err := prepareResourcePayload[apiresourcecontracts.ResourceConfiguration](bytes) + resourceReturn.Resource = payload + return err + } + + if resourceReturn.ApiVersion == "aquasecurity.github.io/v1alpha1" && resourceReturn.Kind == "ClusterComplianceReport" { + payload, err := prepareResourcePayload[apiresourcecontracts.ResourceClusterComplianceReport](bytes) + resourceReturn.Resource = payload + return err + } + + if resourceReturn.ApiVersion == "general.ror.internal/v1alpha1" && resourceReturn.Kind == "ClusterVulnerabilityReport" { + payload, err := prepareResourcePayload[apiresourcecontracts.ResourceClusterVulnerabilityReport](bytes) + resourceReturn.Resource = payload + return err + } + + if resourceReturn.ApiVersion == "general.ror.internal/v1alpha1" && resourceReturn.Kind == "Route" { + payload, err := prepareResourcePayload[apiresourcecontracts.ResourceRoute](bytes) + resourceReturn.Resource = payload + return err + } + + if resourceReturn.ApiVersion == "general.ror.internal/v1alpha1" && resourceReturn.Kind == "SlackMessage" { + payload, err := prepareResourcePayload[apiresourcecontracts.ResourceSlackMessage](bytes) + resourceReturn.Resource = payload + return err + } + + if resourceReturn.ApiVersion == "general.ror.internal/v1alpha1" && resourceReturn.Kind == "VulnerabilityEvent" { + payload, err := prepareResourcePayload[apiresourcecontracts.ResourceVulnerabilityEvent](bytes) + resourceReturn.Resource = payload + return err + } + + if resourceReturn.ApiVersion == "general.ror.internal/v1alpha1" && resourceReturn.Kind == "VirtualMachine" { + payload, err := prepareResourcePayload[apiresourcecontracts.ResourceVirtualMachine](bytes) + resourceReturn.Resource = payload + return err + } + + return fmt.Errorf("no handler found for %s/%s", resourceReturn.ApiVersion, resourceReturn.Kind) +} diff --git a/internal/models/rorResources/extractResource.go.tmpl b/internal/models/rorResources/extractResource.go.tmpl new file mode 100644 index 0000000..0185fdb --- /dev/null +++ b/internal/models/rorResources/extractResource.go.tmpl @@ -0,0 +1,21 @@ +// THIS FILE IS GENERATED, DO NOT EDIT +// ref: build/generator/main.go +package rorResources + +import ( + "fmt" + apiresourcecontracts "github.com/NorskHelsenett/ror/pkg/apicontracts/apiresourcecontracts" +) + +// the function determines which model to match the resource to and call prepareResourcePayload to cast the input to the matching model. +func (rj rorResourceJson) getResource(resourceReturn *rorResource) error { + bytes := []byte(rj) +{{ range .}} + if resourceReturn.ApiVersion == "{{.GetApiVersion}}" && resourceReturn.Kind == "{{.Kind}}" { + payload, err := prepareResourcePayload[apiresourcecontracts.Resource{{.Kind}}](bytes) + resourceReturn.Resource = payload + return err + } +{{end}} + return fmt.Errorf("no handler found for %s/%s", resourceReturn.ApiVersion, resourceReturn.Kind) +} diff --git a/internal/models/rorResources/resourcestype.go b/internal/models/rorResources/resourcestype.go new file mode 100644 index 0000000..e03c5a9 --- /dev/null +++ b/internal/models/rorResources/resourcestype.go @@ -0,0 +1,99 @@ +package rorResources + +import ( + "crypto/md5" // #nosec G501 - MD5 is used for hash calculation only + "encoding/json" + "fmt" + + "github.com/NorskHelsenett/ror/pkg/apicontracts/apiresourcecontracts" + + "github.com/NorskHelsenett/ror/pkg/rlog" + + jsonpatch "github.com/evanphx/json-patch/v5" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +type rorResource struct { + ApiVersion string `json:"apiVersion"` + Kind string `json:"kind"` + Uid string `json:"uid"` + Hash string `json:"hash"` + Resource any `json:"resource"` +} + +type rorResourceJson []byte + +func NewFromUnstructured(input *unstructured.Unstructured) (rorResource, error) { + returnResource := rorResource{ + ApiVersion: input.GetAPIVersion(), + Kind: input.GetKind(), + Uid: string(input.GetUID()), + } + bytes, err := input.MarshalJSON() + jsonData := rorResourceJson(bytes) + if err != nil { + rlog.Error("Could not unmarshal resource", err) + return returnResource, err + } + + err = jsonData.removeUnnecessaryData() + if err != nil { + return returnResource, err + } + returnResource.Hash, err = jsonData.calculateHash() + if err != nil { + return returnResource, err + } + err = jsonData.getResource(&returnResource) + if err != nil { + return returnResource, err + } + return returnResource, nil +} + +func (r rorResource) NewResourceUpdateModel(owner apiresourcecontracts.ResourceOwnerReference, action apiresourcecontracts.ResourceAction) *apiresourcecontracts.ResourceUpdateModel { + return &apiresourcecontracts.ResourceUpdateModel{ + Owner: owner, + ApiVersion: r.ApiVersion, + Kind: r.Kind, + Uid: r.Uid, + Action: action, + Hash: r.Hash, + Resource: r.Resource, + } +} + +func prepareResourcePayload[D any](input []byte) (D, error) { + var outStruct D + err := json.Unmarshal(input, &outStruct) + if err != nil { + rlog.Error("error unmarshaling json", err) + return outStruct, err + } + return outStruct, nil +} + +func (rj rorResourceJson) calculateHash() (string, error) { + bytes := []byte(rj) + patch := []byte(`{"metadata":{"resourceVersion":null,"creationTimestamp":null,"generation":null}}`) + input, err := jsonpatch.MergePatch(bytes, patch) + if err != nil { + rlog.Error("error patching json", err) + return "", err + } + resourceHash := fmt.Sprintf("%x", md5.Sum(input)) // #nosec G401 - MD5 is used for hash calculation only + return resourceHash, nil + +} +func (rj *rorResourceJson) removeUnnecessaryData() error { + bytes := []byte(*rj) + patch := []byte(`{"metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":null}}}`) + bytes, err := jsonpatch.MergePatch(bytes, patch) + if err != nil { + rlog.Error("error patching json", err) + return err + } + *rj = rorResourceJson(bytes) + + return nil +} diff --git a/internal/scheduler/heartbeat.go b/internal/scheduler/heartbeat.go new file mode 100644 index 0000000..ecbab47 --- /dev/null +++ b/internal/scheduler/heartbeat.go @@ -0,0 +1,69 @@ +package scheduler + +import ( + "encoding/json" + + "github.com/NorskHelsenett/ror-agent/internal/clients/clients" + "github.com/NorskHelsenett/ror-agent/internal/config" + + "github.com/NorskHelsenett/ror/pkg/apicontracts" + + "github.com/NorskHelsenett/ror/pkg/rlog" + + "github.com/NorskHelsenett/ror-agent/internal/services" +) + +func HeartbeatReporting() error { + clusterReport, err := services.GetHeartbeatReport() + if err != nil { + rlog.Error("error when getting heartbeat report", err) + return err + } + + err = sendReportToRor(clusterReport) + return err +} + +func sendReportToRor(clusterReport apicontracts.Cluster) error { + rorClient, err := clients.GetOrCreateRorClient() + if err != nil { + config.IncreaseErrorCount() + rlog.Error("could not get ror-api client", err, + rlog.Int("error count", config.ErrorCount)) + return err + } + + url := "/v1/cluster/heartbeat" + response, err := rorClient.R(). + SetHeader("Content-Type", "application/json"). + SetBody(clusterReport). + Post(url) + if err != nil { + config.IncreaseErrorCount() + rlog.Error("could not send data to ror-api", err, + rlog.Int("error count", config.ErrorCount)) + return err + } + + if response == nil { + config.IncreaseErrorCount() + rlog.Error("response is nil", err, + rlog.Int("error count", config.ErrorCount)) + return err + } + + if !response.IsSuccess() { + config.IncreaseErrorCount() + rlog.Error("got unsuccessful status code from ror-api", err, rlog.Int("status code", response.StatusCode())) + return err + } else { + config.ResetErrorCount() + rlog.Info("heartbeat report sent to ror") + + byteReport, err := json.Marshal(clusterReport) + if err == nil { + rlog.Debug("", rlog.String("byte report", string(byteReport))) + } + } + return nil +} diff --git a/internal/scheduler/metrics.go b/internal/scheduler/metrics.go new file mode 100644 index 0000000..235a734 --- /dev/null +++ b/internal/scheduler/metrics.go @@ -0,0 +1,201 @@ +package scheduler + +import ( + "context" + "encoding/json" + "time" + + "github.com/NorskHelsenett/ror-agent/internal/clients/clients" + "github.com/NorskHelsenett/ror-agent/internal/config" + "github.com/NorskHelsenett/ror-agent/internal/services/authservice" + + "github.com/NorskHelsenett/ror/pkg/apicontracts" + "github.com/NorskHelsenett/ror/pkg/apicontracts/apiresourcecontracts" + aclmodels "github.com/NorskHelsenett/ror/pkg/models/acl" + "github.com/NorskHelsenett/ror/pkg/rlog" + + apimachinery "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/client-go/kubernetes" +) + +func MetricsReporting() error { + k8sClient, err := clients.Kubernetes.GetKubernetesClientset() + if err != nil { + return err + } + var metricsReport apicontracts.MetricsReport + + metricsReportNodes, err := CreateNodeMetricsList(k8sClient) + if err != nil { + rlog.Error("error converting podmetrics", err) + return err + } + ownerref := authservice.CreateOwnerref() + + metricsReport.Owner = apiresourcecontracts.ResourceOwnerReference{ + Scope: aclmodels.Acl2Scope(ownerref.Scope), + Subject: string(ownerref.Subject), + } + metricsReport.Nodes = metricsReportNodes + + err = sendMetricsToRor(metricsReport) + + return err +} + +func sendMetricsToRor(metricsReport apicontracts.MetricsReport) error { + rorClient, err := clients.GetOrCreateRorClient() + if err != nil { + rlog.Error("Could not get ror-api client", err) + config.IncreaseErrorCount() + return err + } + + url := "/v1/metrics" + response, err := rorClient.R(). + SetHeader("Content-Type", "application/json"). + SetBody(metricsReport). + Post(url) + if err != nil { + rlog.Error("Could not send metrics data to ror-api", err) + config.IncreaseErrorCount() + return err + } + + if response == nil { + rlog.Error("Response is nil", err) + config.IncreaseErrorCount() + return err + } + + if !response.IsSuccess() { + config.IncreaseErrorCount() + rlog.Error("Got unsuccessful status code from ror-api", err, + rlog.Int("status code", response.StatusCode()), + rlog.Int("error count", config.ErrorCount)) + return err + } else { + config.ResetErrorCount() + rlog.Info("Metrics report sent to ror") + + byteReport, err := json.Marshal(metricsReport) + if err == nil { + rlog.Debug("", rlog.String("byte report", string(byteReport))) + } + } + return nil +} + +func CreateNodeMetricsList(k8sClient *kubernetes.Clientset) ([]apicontracts.NodeMetric, error) { + var nodeMetricsList apicontracts.NodeMetricsList + var metricsReportNodes []apicontracts.NodeMetric + + data, err := k8sClient.RESTClient().Get().AbsPath("apis/metrics.k8s.io/v1beta1/nodes").DoRaw(context.TODO()) + if err != nil { + rlog.Error("error converting podmetrics", err) + return metricsReportNodes, err + } + + err = json.Unmarshal(data, &nodeMetricsList) + if err != nil { + rlog.Error("error unmarshaling podmetrics", err) + return metricsReportNodes, err + } + + for _, node := range nodeMetricsList.Items { + + metricsReportNode, err := CreateNodeMetrics(node) + if err != nil { + rlog.Error("error converting podmetrics", err) + return metricsReportNodes, err + } + metricsReportNodes = append(metricsReportNodes, metricsReportNode) + } + + return metricsReportNodes, nil + +} + +func CreateNodeMetrics(node apicontracts.NodeMetricsListItem) (apicontracts.NodeMetric, error) { + var nodeMetric apicontracts.NodeMetric + var timestamp time.Time = node.Timestamp + + nodeCpuRaw, err := apimachinery.ParseQuantity(node.Usage.CPU) + if err != nil { + rlog.Error("error converting nodemetrics", err) + return nodeMetric, err + } + nodeCpu := nodeCpuRaw.MilliValue() + + nodeMemoryRaw, err := apimachinery.ParseQuantity(node.Usage.Memory) + if err != nil { + rlog.Error("error converting nodemetrics", err) + return nodeMetric, err + } + nodeMemory, _ := nodeMemoryRaw.AsInt64() + + nodeMetric = apicontracts.NodeMetric{ + Name: node.Metadata.Name, + TimeStamp: timestamp, + CpuUsage: nodeCpu, + MemoryUsage: nodeMemory, + } + return nodeMetric, nil +} +func CreatePodMetricsList(k8sClient *kubernetes.Clientset) ([]apicontracts.PodMetric, error) { + var podMetricsList apicontracts.PodMetricsList + var metricsReportPods []apicontracts.PodMetric + + data, err := k8sClient.RESTClient().Get().AbsPath("apis/metrics.k8s.io/v1beta1/pods").DoRaw(context.TODO()) + if err != nil { + rlog.Error("error unmarshaling podmetrics", err) + return metricsReportPods, err + } + + err = json.Unmarshal(data, &podMetricsList) + if err != nil { + rlog.Error("error unmarshaling podmetrics", err) + return metricsReportPods, err + } + + for _, pod := range podMetricsList.Items { + metricsReportPod, err := CreatePodMetrics(pod) + if err != nil { + rlog.Error("error converting podmetrics", err) + return metricsReportPods, err + } + metricsReportPods = append(metricsReportPods, metricsReportPod) + } + return metricsReportPods, nil +} + +func CreatePodMetrics(pod apicontracts.PodMetricsListItem) (apicontracts.PodMetric, error) { + var podMetric apicontracts.PodMetric + var timestamp time.Time = pod.Timestamp + var podCpuSum int64 = 0 + var podMemorySum int64 = 0 + + for _, container := range pod.Containers { + podCpu, err := apimachinery.ParseQuantity(container.Usage.CPU) + if err != nil { + rlog.Error("error converting podmetrics", err) + return podMetric, err + } + podCpuSum = podCpuSum + podCpu.MilliValue() + podMemoryObj, err := apimachinery.ParseQuantity(container.Usage.Memory) + if err != nil { + rlog.Error("error converting podmetrics", err) + return podMetric, err + } + podMemory, _ := podMemoryObj.AsInt64() + podMemorySum = podMemorySum + podMemory + } + podMetric = apicontracts.PodMetric{ + Name: pod.Metadata.Name, + Namespace: pod.Metadata.Namespace, + TimeStamp: timestamp, + CpuUsage: podCpuSum, + MemoryUsage: podMemorySum, + } + return podMetric, nil +} diff --git a/internal/scheduler/setup.go b/internal/scheduler/setup.go new file mode 100644 index 0000000..10b97c1 --- /dev/null +++ b/internal/scheduler/setup.go @@ -0,0 +1,24 @@ +package scheduler + +import ( + "time" + + "github.com/NorskHelsenett/ror/pkg/rlog" + + "github.com/go-co-op/gocron" +) + +func SetUpScheduler() { + scheduler := gocron.NewScheduler(time.UTC) + _, err := scheduler.Every(1).Minute().Tag("heartbeat").Do(HeartbeatReporting) + if err != nil { + rlog.Fatal("Failed to setup heartbeat schedule", err) + } + + _, err = scheduler.Every(1).Minute().Tag("metrics").Do(MetricsReporting) + if err != nil { + rlog.Fatal("Failed to setup metric schedule", err) + } + _ = scheduler.RunByTag("heartbeat") + scheduler.StartAsync() +} diff --git a/internal/services/authservice/authservice.go b/internal/services/authservice/authservice.go new file mode 100644 index 0000000..553f116 --- /dev/null +++ b/internal/services/authservice/authservice.go @@ -0,0 +1,20 @@ +// authservice implements authorization helpers for the agent +package authservice + +import ( + "github.com/NorskHelsenett/ror/pkg/config/configconsts" + + aclmodels "github.com/NorskHelsenett/ror/pkg/models/acl" + + "github.com/NorskHelsenett/ror/pkg/apicontracts/apiresourcecontracts" + + "github.com/spf13/viper" +) + +// creaters a ownerref object for the agent +func CreateOwnerref() apiresourcecontracts.ResourceOwnerReference { + return apiresourcecontracts.ResourceOwnerReference{ + Scope: aclmodels.Acl2ScopeCluster, + Subject: viper.GetString(configconsts.CLUSTER_ID), + } +} diff --git a/internal/services/heartbeat.go b/internal/services/heartbeat.go new file mode 100644 index 0000000..a0e6193 --- /dev/null +++ b/internal/services/heartbeat.go @@ -0,0 +1,452 @@ +package services + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + "github.com/NorskHelsenett/ror-agent/internal/clients/clients" + "github.com/NorskHelsenett/ror-agent/internal/kubernetes/k8smodels" + "github.com/NorskHelsenett/ror-agent/internal/kubernetes/nodeservice" + "github.com/NorskHelsenett/ror-agent/internal/models/argomodels" + "github.com/NorskHelsenett/ror-agent/internal/utils" + + "github.com/NorskHelsenett/ror/pkg/config/configconsts" + + "github.com/NorskHelsenett/ror/pkg/apicontracts" + "github.com/NorskHelsenett/ror/pkg/models/providers" + + "github.com/NorskHelsenett/ror/pkg/rlog" + + "github.com/spf13/viper" + "gopkg.in/yaml.v3" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" +) + +var MissingConst = "Missing ..." + +func GetHeartbeatReport() (apicontracts.Cluster, error) { + + k8sClient, err := clients.Kubernetes.GetKubernetesClientset() + if err != nil { + return apicontracts.Cluster{}, err + } + + dynamicClient, err := clients.Kubernetes.GetDynamicClient() + if err != nil { + return apicontracts.Cluster{}, err + } + metricsClient, err := clients.Kubernetes.GetMetricsClient() + if err != nil { + return apicontracts.Cluster{}, err + } + + clusterName := "localhost" + workspaceName := "localhost" + datacenterName := "local" + provider := providers.ProviderTypeUnknown + + nhnToolingMetadata, err := getNhnToolingMetadata(k8sClient, dynamicClient) + if err != nil { + rlog.Warn("NHN-Tooling is not installed?!") + } + + k8sVersion, err := k8sClient.ServerVersion() + if err != nil { + rlog.Error("could not get kubernetes server version", err) + } + + kubernetesVersion := MissingConst + if k8sVersion != nil { + kubernetesVersion = k8sVersion.String() + } + + k8sVersionArray := strings.Split(kubernetesVersion, "+") + if len(k8sVersionArray) > 1 { + kubernetesVersion = k8sVersionArray[0] + } + + nodes, err := nodeservice.GetNodes(k8sClient, metricsClient) + if err != nil { + rlog.Error("error getting nodes", err) + } + + var k8sControlPlaneEndpoint string + var controlPlane = apicontracts.ControlPlane{} + nodePools := make([]apicontracts.NodePool, 0) + if len(nodes) > 0 { + for _, node := range nodes { + if _, ok := node.Labels["node-role.kubernetes.io/control-plane"]; ok { + appendNodeToControlePlane(&node, &controlPlane) + if node.Provider == "tanzu" { + k8sControlPlaneEndpoint, _ = getControlPlaneEndpoint(k8sClient) + } + } else { + appendNodeToNodePools(&nodePools, &node) + } + } + if err != nil { + rlog.Error("could not extract controlplane and workers from nodes", err) + } + } + + if len(nodes) > 0 { + firstNode := nodes[0] + clusterName = firstNode.ClusterName + workspaceName = firstNode.Workspace + datacenterName = firstNode.Datacenter + provider = firstNode.Provider + } + + ingresses, err := getIngresses(k8sClient) + if err != nil { + rlog.Error("could not get ingresses", err) + } + + nodeCount := int64(0) + cpuSum := int64(0) + cpuConsumedSum := int64(0) + memorySum := int64(0) + memoryConsumedSum := int64(0) + for i := 0; i < len(nodePools); i++ { + nodepool := nodePools[i] + nodeCount = nodeCount + nodepool.Metrics.NodeCount + cpuSum = cpuSum + nodepool.Metrics.Cpu + cpuConsumedSum = cpuConsumedSum + nodepool.Metrics.CpuConsumed + memorySum = memorySum + nodepool.Metrics.Memory + memoryConsumedSum = memoryConsumedSum + nodepool.Metrics.MemoryConsumed + } + + agentVersion := viper.GetString(configconsts.VERSION) + agentSha := viper.GetString(configconsts.COMMIT) + + var created time.Time + kubeSystem := "kube-system" + kubeSystemNamespace, err := k8sClient.CoreV1().Namespaces().Get(context.Background(), kubeSystem, v1.GetOptions{}) + if err != nil { + rlog.Error("could not fetch namespace", err, rlog.String("namespace", kubeSystem)) + } else { + created = kubeSystemNamespace.CreationTimestamp.Time + } + + report := apicontracts.Cluster{ + ACL: apicontracts.AccessControlList{ + AccessGroups: nhnToolingMetadata.AccessGroups, + }, + Environment: nhnToolingMetadata.Environment, + ClusterId: viper.GetString(configconsts.CLUSTER_ID), + ClusterName: clusterName, + Ingresses: ingresses, + Created: created, + Topology: apicontracts.Topology{ + ControlPlaneEndpoint: k8sControlPlaneEndpoint, + EgressIp: EgressIp, + ControlPlane: controlPlane, + NodePools: nodePools, + }, + Versions: apicontracts.Versions{ + Kubernetes: kubernetesVersion, + NhnTooling: apicontracts.NhnTooling{ + Version: nhnToolingMetadata.Version, + Branch: nhnToolingMetadata.Branch, + Environment: nhnToolingMetadata.Environment, + }, + Agent: apicontracts.Agent{ + Version: agentVersion, + Sha: agentSha, + }, + }, + Metrics: apicontracts.Metrics{ + NodeCount: nodeCount, + NodePoolCount: int64(len(nodePools)), + Cpu: cpuSum, + CpuConsumed: cpuConsumedSum, + Memory: memorySum, + MemoryConsumed: memoryConsumedSum, + ClusterCount: 1, + }, + Workspace: apicontracts.Workspace{ + Name: workspaceName, + Datacenter: apicontracts.Datacenter{ + Name: datacenterName, + Provider: provider, + }, + }, + } + return report, nil +} + +func getIngresses(k8sClient *kubernetes.Clientset) ([]apicontracts.Ingress, error) { + var ingressList []apicontracts.Ingress + nsList, err := k8sClient.CoreV1().Namespaces().List(context.TODO(), v1.ListOptions{}) + if err != nil { + rlog.Error("could not fetch namespaces", err) + return ingressList, errors.New("could not fetch namespaces from cluster") + } + + for _, namespace := range nsList.Items { + ing := k8sClient.NetworkingV1().Ingresses(namespace.Name) + ingresses, err := ing.List(context.TODO(), v1.ListOptions{}) + if err != nil { + rlog.Error("could not list ingress in namespace", err, rlog.String("namespace", namespace.Name)) + continue + } + for _, ingress := range ingresses.Items { + + richIngress, err := utils.GetIngressDetails(&ingress) + if err != nil { + rlog.Error("could not enrich ingress", err, + rlog.String("ingress", ingress.Name), + rlog.String("namespace", namespace.Name)) + continue + } else { + ingressList = append(ingressList, *richIngress) + } + } + } + + return ingressList, nil +} + +func getNhnToolingMetadata(k8sClient *kubernetes.Clientset, dynamicClient dynamic.Interface) (k8smodels.NhnTooling, error) { + var accessGroups []string + result := k8smodels.NhnTooling{ + Version: MissingConst, + Branch: MissingConst, + AccessGroups: []string{}, + Environment: "dev", + } + + namespace := viper.GetString(configconsts.POD_NAMESPACE) + nhnToolingConfigMap, err := k8sClient.CoreV1().ConfigMaps(namespace).Get(context.TODO(), "nhn-tooling", v1.GetOptions{ + TypeMeta: v1.TypeMeta{}, + ResourceVersion: "", + }) + + if err != nil { + return result, fmt.Errorf("could not find config map %s for ror in namespace %s", "nhn-tooling", namespace) + } + + if nhnToolingConfigMap.Data == nil { + return result, errors.New("no data in config map for ror") + } + + environment := nhnToolingConfigMap.Data["environment"] + toolingVersion := nhnToolingConfigMap.Data["toolingVersion"] + accessGroupsValue := nhnToolingConfigMap.Data["accessGroups"] + if accessGroupsValue != "" { + accessGroups = strings.Split(accessGroupsValue, ";") + } + + if environment == "" { + environment = "dev" + } + + if toolingVersion == "" { + toolingVersion = MissingConst + } + + branch := MissingConst + nhnToolingApp, err := getNhnToolingInfo(dynamicClient) + if err != nil { + rlog.Error("could not get nhn-tooling application", err) + } else { + branch = nhnToolingApp.Spec.Source.TargetRevision + if len(nhnToolingApp.Status.Sync.Revision) < 20 { + toolingVersion = nhnToolingApp.Status.Sync.Revision + } + } + + result.Version = toolingVersion + result.Environment = environment + result.AccessGroups = accessGroups + result.Branch = branch + + return result, nil +} + +func getNhnToolingInfo(dynamicClient dynamic.Interface) (argomodels.Application, error) { + result := argomodels.Application{} + applications, err := dynamicClient.Resource(schema.GroupVersionResource{ + Group: "argoproj.io", + Version: "v1alpha1", + Resource: "applications", + }). + Namespace("argocd"). + Get(context.TODO(), "nhn-tooling", v1.GetOptions{}) + if err != nil { + rlog.Error("could not get nhn-tooling application", err) + return result, err + } + + appByteArray, err := applications.MarshalJSON() + if err != nil { + rlog.Error("could not marshal application", err) + return result, err + } + + var nhnTooling argomodels.Application + err = json.Unmarshal(appByteArray, &nhnTooling) + if err != nil { + rlog.Error("could not marshal applications", err) + return result, err + } + + if nhnTooling.Metadata.Name == "" { + return result, errors.New("could not find nhn-tooling application") + } + + return nhnTooling, nil +} + +func appendNodeToControlePlane(node *k8smodels.Node, controlPlane *apicontracts.ControlPlane) { + apiNode := apicontracts.Node{ + Name: node.Name, + Role: "control-plane", + Created: node.Created, + OsImage: node.OsImage, + MachineName: node.MachineName, + Architecture: node.Architecture, + ContainerRuntimeVersion: node.ContainerRuntimeVersion, + KernelVersion: node.KernelVersion, + KubeProxyVersion: node.KubeProxyVersion, + KubeletVersion: node.KubeletVersion, + OperatingSystem: node.OperatingSystem, + Metrics: apicontracts.Metrics{ + Cpu: node.Resources.Allocated.Cpu, + Memory: node.Resources.Allocated.MemoryBytes, + CpuConsumed: node.Resources.Consumed.CpuMilliValue, + MemoryConsumed: node.Resources.Consumed.MemoryBytes, + }, + } + + controlPlane.Nodes = append(controlPlane.Nodes, apiNode) + + controlPlane.Metrics.NodeCount = int64(len(controlPlane.Nodes)) + controlPlane.Metrics.Cpu = controlPlane.Metrics.Cpu + apiNode.Metrics.Cpu + controlPlane.Metrics.Memory = controlPlane.Metrics.Memory + apiNode.Metrics.Memory + controlPlane.Metrics.CpuConsumed = controlPlane.Metrics.CpuConsumed + apiNode.Metrics.CpuConsumed + controlPlane.Metrics.MemoryConsumed = controlPlane.Metrics.MemoryConsumed + apiNode.Metrics.MemoryConsumed +} + +func appendNodeToNodePools(nodePools *[]apicontracts.NodePool, node *k8smodels.Node) { + rlog.Debug("", rlog.String("Clustername", node.ClusterName)) + clusterNameSplit := strings.Split(node.ClusterName, "-") + machineNameSplit := strings.Split(node.MachineName, "-") + rlog.Debug("", rlog.Strings("machine name split", machineNameSplit)) + var workerName string + if node.Provider == providers.ProviderTypeTalos { + workerName = node.Name + } else if node.Provider != providers.ProviderTypeAks { + workerName = machineNameSplit[len(clusterNameSplit)] + } else { + workerName = machineNameSplit[1] + } + + rlog.Debug("", rlog.String("worker name", workerName)) + + apiNode := apicontracts.Node{ + Role: "worker", + Name: node.Name, + Created: node.Created, + OsImage: node.OsImage, + MachineName: node.MachineName, + Architecture: node.Architecture, + ContainerRuntimeVersion: node.ContainerRuntimeVersion, + KernelVersion: node.KernelVersion, + KubeProxyVersion: node.KubeProxyVersion, + KubeletVersion: node.KubeletVersion, + OperatingSystem: node.OperatingSystem, + + Metrics: apicontracts.Metrics{ + Cpu: node.Resources.Allocated.Cpu, + CpuConsumed: node.Resources.Consumed.CpuMilliValue, + Memory: node.Resources.Allocated.MemoryBytes, + MemoryConsumed: node.Resources.Consumed.MemoryBytes, + }, + } + + var nodePool *apicontracts.NodePool = nil + var index int + for i := 0; i < len(*nodePools); i++ { + nodepool := (*nodePools)[i] + if nodepool.Name == workerName { + index = i + nodePool = &nodepool + } + } + + if nodePool == nil { + list := []apicontracts.Node{apiNode} + np := apicontracts.NodePool{ + Name: workerName, + Nodes: list, + Metrics: apicontracts.Metrics{ + NodeCount: int64(len(list)), + Cpu: apiNode.Metrics.Cpu, + Memory: apiNode.Metrics.Memory, + CpuConsumed: apiNode.Metrics.CpuConsumed, + MemoryConsumed: apiNode.Metrics.MemoryConsumed, + }, + } + *nodePools = append(*nodePools, np) + } else { + nodelist := append(nodePool.Nodes, apiNode) + nodePool.Nodes = nodelist + nodePool.Metrics.Cpu = nodePool.Metrics.Cpu + apiNode.Metrics.Cpu + nodePool.Metrics.Memory = nodePool.Metrics.Memory + apiNode.Metrics.Memory + nodePool.Metrics.CpuConsumed = nodePool.Metrics.CpuConsumed + apiNode.Metrics.CpuConsumed + nodePool.Metrics.MemoryConsumed = nodePool.Metrics.MemoryConsumed + apiNode.Metrics.MemoryConsumed + nodePool.Metrics.NodeCount = int64(len(nodelist)) + (*nodePools)[index] = *nodePool + } +} + +func getControlPlaneEndpoint(clientset *kubernetes.Clientset) (string, error) { + kubeadmConfigMap, err := clientset.CoreV1().ConfigMaps("kube-system").Get(context.TODO(), "kubeadm-config", v1.GetOptions{}) + if err != nil { + errMsg := "getControlPlaneEndpoint: Could not get cluster config from kube-system/kubeadm-config, check rbac" + return "", errors.New(errMsg) + } + + if kubeadmConfigMap == nil { + errMsg := "getControlPlaneEndpoint: get value 'ControlPlaneEndpoint' from yaml" + return "", errors.New(errMsg) + } + + kubeadmClusterConfiguration := kubeadmConfigMap.Data["ClusterConfiguration"] + + var clusterConfigurationValues K8sClusterConfiguration + err = yaml.Unmarshal([]byte(kubeadmClusterConfiguration), &clusterConfigurationValues) + if err != nil { + errMsg := "getControlPlaneEndpoint: Could not parse yaml string to stuct" + rlog.Error(errMsg, err) + return "", errors.New(errMsg) + } + + return clusterConfigurationValues.ControlPlaneEndpoint, nil +} + +type K8sClusterConfiguration struct { + ControlPlaneEndpoint string `yaml:"controlPlaneEndpoint"` +} +type NhnToolingValues struct { + Cluster NhnToolingCluster `yaml:"cluster"` + NHN NHN `yaml:"nhn"` +} + +type NhnToolingCluster struct { + AccessGroups []string `yaml:"accessGroups"` +} + +type NHN struct { + AccessGroups []string `yaml:"accessGroups"` + ToolingVersion string `yaml:"toolingVersion"` + Environment string `yaml:"environment"` +} diff --git a/internal/services/initial_service.go b/internal/services/initial_service.go new file mode 100644 index 0000000..bdf5441 --- /dev/null +++ b/internal/services/initial_service.go @@ -0,0 +1,131 @@ +package services + +import ( + "context" + "errors" + "fmt" + "io" + "net" + "net/http" + "strings" + + "github.com/NorskHelsenett/ror-agent/internal/clients/clients" + "github.com/NorskHelsenett/ror-agent/internal/kubernetes/operator/initialize" + + "github.com/NorskHelsenett/ror/pkg/config/configconsts" + + "github.com/NorskHelsenett/ror/pkg/rlog" + + "github.com/spf13/viper" + coreV1 "k8s.io/api/core/v1" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "k8s.io/client-go/kubernetes" + metrics "k8s.io/metrics/pkg/client/clientset/versioned" +) + +var EgressIp string + +func FetchApikey(k8sClient *kubernetes.Clientset, metricsClient *metrics.Clientset) (string, error) { + clusterInfo, err := initialize.GetClusterInfoFromNode(k8sClient, metricsClient) + if err != nil { + rlog.Error("could not get identifier", err) + return "", errors.New("could not get identifier") + } + + rorUrl := viper.GetString(configconsts.API_ENDPOINT) + apikey, err := initialize.GetApikey(clusterInfo, rorUrl) + if err != nil { + rlog.Error("not able to get api key", err, + rlog.String("clusterName", clusterInfo.ClusterName), + rlog.String("ror url", rorUrl)) + + return "", fmt.Errorf("could not fetch api key from API (url: %s)", rorUrl) + } + viper.Set(configconsts.API_KEY, apikey) + return apikey, nil +} + +func ExtractApikeyOrDie() error { + k8sClient, err := clients.Kubernetes.GetKubernetesClientset() + if err != nil { + return err + } + + metricsClient, err := clients.Kubernetes.GetMetricsClient() + if err != nil { + return err + } + + secretName := viper.GetString(configconsts.API_KEY_SECRET) + namespace := viper.GetString(configconsts.POD_NAMESPACE) + secretApiKey := "APIKEY" + secret, err := k8sClient.CoreV1().Secrets(namespace).Get(context.TODO(), secretName, metaV1.GetOptions{}) + if err != nil { + apikey, err := FetchApikey(k8sClient, metricsClient) + if err != nil { + rlog.Error("could not fetch api key: ", err) + return errors.New("could not fetch api key") + } + secret, err = k8sClient.CoreV1().Secrets(namespace).Create(context.TODO(), + &coreV1.Secret{ + ObjectMeta: metaV1.ObjectMeta{ + Name: secretName, + Namespace: namespace, + }, + Type: "Opaque", + StringData: map[string]string{ + secretApiKey: apikey, + }, + }, + metaV1.CreateOptions{}) + if err != nil { + rlog.Error("could not create k8s secret: ", err) + return errors.New("could not create secret") + } + } + + apikey := string(secret.Data[secretApiKey]) + viper.Set(configconsts.API_KEY, apikey) + + return nil +} + +func GetEgressIp() { + internettCheck := "https://api.ipify.org/" + nhnCheck := "ip.nhn.no" + _, err := net.LookupIP(nhnCheck) + var apiHost string + if err != nil { + apiHost = internettCheck + } else { + apiHost = fmt.Sprintf("http://%s", nhnCheck) + } + + rlog.Info("Resolving ip", rlog.String("api host", apiHost)) + res, err := http.Get(apiHost) // #nosec G107 - we are not using user input + if err != nil { + // assuming retry but on internett + apiHost = internettCheck + res, err = http.Get(apiHost) // #nosec G107 - we are not using user input + if err != nil { + errorMsg := fmt.Sprintf("could not reach host %s", apiHost) + rlog.Info(errorMsg) + return + } + } + + body, err := io.ReadAll(res.Body) + _ = res.Body.Close() + if res.StatusCode > 299 { + rlog.Info("response failed", rlog.Int("status code", res.StatusCode), rlog.ByteString("body", body)) + return + } + + if err != nil { + rlog.Error("could not parse body", err) + return + } + + EgressIp = strings.Replace(string(body), "\n", "", -1) +} diff --git a/internal/services/resourceupdatev2/client.go b/internal/services/resourceupdatev2/client.go new file mode 100644 index 0000000..2d7dd93 --- /dev/null +++ b/internal/services/resourceupdatev2/client.go @@ -0,0 +1,126 @@ +package resourceupdatev2 + +import ( + "encoding/json" + + "github.com/NorskHelsenett/ror-agent/internal/clients/clients" + "github.com/NorskHelsenett/ror-agent/internal/config" + "github.com/NorskHelsenett/ror-agent/internal/services/authservice" + + "github.com/NorskHelsenett/ror/pkg/apicontracts/apiresourcecontracts" + + "github.com/NorskHelsenett/ror/pkg/rlog" + + "github.com/go-resty/resty/v2" +) + +// the function sends the resource to the ror api. If recieving a non 2xx statuscode it will retun an error. +func sendResourceUpdateToRor(resourceUpdate *apiresourcecontracts.ResourceUpdateModel) error { + rorClient, err := clients.GetOrCreateRorClient() + if err != nil { + rlog.Error("Could not get ror-api client", err) + config.IncreaseErrorCount() + return err + } + var url string + var response *resty.Response + + if resourceUpdate.Action == apiresourcecontracts.K8sActionAdd { + url = "/v1/resources" + response, err = rorClient.R(). + SetHeader("Content-Type", "application/json"). + SetBody(resourceUpdate). + Post(url) + + } else if resourceUpdate.Action == apiresourcecontracts.K8sActionUpdate { + url = "/v1/resources/uid/" + resourceUpdate.Uid + response, err = rorClient.R(). + SetHeader("Content-Type", "application/json"). + SetBody(resourceUpdate). + Put(url) + } else if resourceUpdate.Action == apiresourcecontracts.K8sActionDelete { + url = "/v1/resources/uid/" + resourceUpdate.Uid + response, err = rorClient.R(). + SetHeader("Content-Type", "application/json"). + SetBody(resourceUpdate). + Delete(url) + } + + if err != nil { + config.IncreaseErrorCount() + rlog.Error("could not send data to ror-api", err, + rlog.Int("error count", config.ErrorCount)) + return err + } + + if response == nil { + config.IncreaseErrorCount() + rlog.Error("response is nil", err, + rlog.Int("error count", config.ErrorCount)) + return err + } + + if !response.IsSuccess() { + config.IncreaseErrorCount() + rlog.Info("got non 200 statuscode from ror-api", rlog.Int("status code", response.StatusCode()), + rlog.Int("error count", config.ErrorCount)) + return err + } else { + config.ResetErrorCount() + rlog.Debug("partial update sent to ror", rlog.String("api verson", resourceUpdate.ApiVersion), rlog.String("kind", resourceUpdate.Kind), rlog.String("uid", resourceUpdate.Uid)) + } + return nil +} + +// function to get the persisted list of hashes from the api. The function is called on startup to populate the internal hashlist. +// The function makes the agent able to catch up on changes that has happened when its offline exluding deletes. +// TODO: Create a check to remove objects that are deleted during downtime of the agent. +func getResourceHashList() (hashList, error) { + var hashlist hashList + + rorClient, err := clients.GetOrCreateRorClient() + if err != nil { + config.IncreaseErrorCount() + rlog.Error("could not get ror-api client", err, + rlog.Int("error count", config.ErrorCount)) + return hashlist, err + } + url := "/v1/resources/hashes" + + ownerref := authservice.CreateOwnerref() + + response, err := rorClient.R(). + SetQueryParams(ownerref.GetQueryParams()). + SetHeader("Content-Type", "application/json"). + Get(url) + + if err != nil { + config.IncreaseErrorCount() + rlog.Error("could not send data to ror-api", err, + rlog.Int("error count", config.ErrorCount)) + return hashlist, err + } + + if response == nil { + config.IncreaseErrorCount() + rlog.Error("response is nil", err, + rlog.Int("error count", config.ErrorCount)) + return hashlist, err + } + + if !response.IsSuccess() { + config.IncreaseErrorCount() + rlog.Info("got non 200 statuscode from ror-api", rlog.Int("status code", response.StatusCode()), + rlog.Int("error count", config.ErrorCount)) + return hashlist, err + } else { + config.ResetErrorCount() + rlog.Info("hashList fetched from ror-api") + + err = json.Unmarshal(response.Body(), &hashlist) + if err != nil { + rlog.Error("could not unmarshal reply", err) + } + } + return hashlist, nil +} diff --git a/internal/services/resourceupdatev2/docs.go b/internal/services/resourceupdatev2/docs.go new file mode 100644 index 0000000..d3ac439 --- /dev/null +++ b/internal/services/resourceupdatev2/docs.go @@ -0,0 +1,3 @@ +// package: resourceupdatev2 +// Path: cmd/agent/services/resourceupdatev2/resourceupdate.go +package resourceupdatev2 diff --git a/internal/services/resourceupdatev2/hashlist.go b/internal/services/resourceupdatev2/hashlist.go new file mode 100644 index 0000000..0d501e1 --- /dev/null +++ b/internal/services/resourceupdatev2/hashlist.go @@ -0,0 +1,77 @@ +package resourceupdatev2 + +import ( + "github.com/NorskHelsenett/ror/pkg/rlog" +) + +// Hashlist for use in agent communication +type hashList struct { + Items []hashItem `json:"items"` +} + +// Item for use in the hashlist +type hashItem struct { + Uid string `json:"uid"` + Hash string `json:"hash"` + Active bool +} + +func (hl hashList) getInactiveUid() []string { + var ret []string + if len(hl.Items) == 0 { + return ret + } + for i := range hl.Items { + if !hl.Items[i].Active { + ret = append(ret, hl.Items[i].Uid) + } + } + return ret +} + +func (hl *hashList) markActive(uid string) { + item, i := hl.getHashByUid(uid) + if item.Uid != "" { + hl.Items[i].Active = true + } + +} + +// Returns a bool value of true if the resource need to be commited +func (rc hashList) checkUpdateNeeded(uid string, hash string) bool { + hashitem, _ := rc.getHashByUid(uid) + if hashitem.Hash == hash { + rlog.Debug("No need to update, hash matched") + return false + } else { + return true + } +} +func (hl hashList) getHashByUid(uid string) (hashItem, int) { + if len(hl.Items) > 0 { + for i := range hl.Items { + if hl.Items[i].Uid == uid { + return hl.Items[i], i + } + } + } + return hashItem{}, 0 +} + +// updates hash in internal hashlist on update. The api will update its list on commiting the resource to its database. +func (hl *hashList) updateHash(uid string, hash string) { + _, i := hl.getHashByUid(uid) + if i != 0 { + rlog.Debug("Update needed, hash updated", rlog.String("uid", uid)) + hl.Items[i].Hash = hash + return + } + rlog.Debug("Uid not found in hashList, adding hash", rlog.String("uid", uid)) + + newItem := hashItem{ + Uid: uid, + Hash: hash, + Active: true, + } + hl.Items = append(hl.Items, newItem) +} diff --git a/internal/services/resourceupdatev2/hashlist_test.go b/internal/services/resourceupdatev2/hashlist_test.go new file mode 100644 index 0000000..61f5a1e --- /dev/null +++ b/internal/services/resourceupdatev2/hashlist_test.go @@ -0,0 +1,427 @@ +package resourceupdatev2 + +import ( + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stretchr/testify/assert" +) + +func Test_hashList_markActive(t *testing.T) { + + type fields struct { + Items []hashItem + } + type args struct { + uid string + } + + testfields := fields{ + Items: []hashItem{ + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120002", + Hash: "1234", + Active: false, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120022", + Hash: "12345", + Active: false, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac130022", + Hash: "12346", + Active: false, + }, + }, + } + testfieldsupdated := fields{ + Items: []hashItem{ + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120002", + Hash: "1234", + Active: true, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120022", + Hash: "12345", + Active: false, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac130022", + Hash: "12346", + Active: false, + }, + }, + } + + tests := []struct { + name string + fields fields + args args + want fields + }{ + { + name: "Test markActive valid input", + fields: testfields, + args: args{ + uid: "3c99c410-3cdd-11ee-be56-0242ac120002", + }, + want: testfieldsupdated, + }, { + name: "Test markActive on unknown uid", + fields: testfields, + args: args{ + uid: "3c99c410-3cdd-11ee-be56-0242ac120012", + }, + want: testfields, + }, { + name: "Test markActive on empty hashlist", + fields: fields{ + Items: []hashItem{}, + }, + args: args{ + uid: "3c99c410-3cdd-11ee-be56-0242ac120012", + }, + want: fields{ + Items: []hashItem{}, + }, + }, { + name: "Test markActive on empty uid", + fields: testfields, + args: args{ + uid: "", + }, + want: testfields, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + hl := &hashList{ + Items: tt.fields.Items, + } + hl.markActive(tt.args.uid) + assert.Equal(t, hl.Items, tt.want.Items) + }) + } +} + +func Test_hashList_getHashByUid(t *testing.T) { + type fields struct { + Items []hashItem + } + type args struct { + uid string + } + tests := []struct { + name string + fields fields + args args + want hashItem + want1 int + }{ + { + name: "Test getHashByUid, empty hashlist", + fields: fields{ + Items: []hashItem{}, + }, + args: args{ + uid: "3c99c410-3cdd-11ee-be56-0242ac120012", + }, + want: hashItem{}, + want1: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + hl := hashList{ + Items: tt.fields.Items, + } + got, got1 := hl.getHashByUid(tt.args.uid) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("hashList.getHashByUid() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("hashList.getHashByUid() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + +func Test_hashList_getInactiveUid(t *testing.T) { + type fields struct { + Items []hashItem + } + tests := []struct { + name string + fields fields + want []string + }{ + { + name: "getInactive", + fields: fields{ + Items: []hashItem{ + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120002", + Hash: "dabfadfd", + Active: false, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120012", + Hash: "dabfadf3", + Active: false, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac12022", + Hash: "dab6fadf", + Active: true, + }, + }, + }, + want: []string{ + "3c99c410-3cdd-11ee-be56-0242ac120002", + "3c99c410-3cdd-11ee-be56-0242ac120012", + }, + }, { + name: "getInactive - empty result", + fields: fields{ + Items: []hashItem{ + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120002", + Hash: "dabfadfd", + Active: true, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120012", + Hash: "dabfadf3", + Active: true, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac12022", + Hash: "dab6fadf", + Active: true, + }, + }, + }, + want: []string{}, + }, { + name: "getInactive - empty list", + fields: fields{ + Items: []hashItem{}, + }, + want: []string{}, + }, + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + hl := hashList{ + Items: tt.fields.Items, + } + if !cmp.Equal(hl.getInactiveUid(), tt.want, cmpopts.EquateEmpty()) { + t.Errorf("%s failed", tt.name) + } + }) + } +} + +func Test_hashList_checkUpdateNeeded(t *testing.T) { + type fields struct { + Items []hashItem + } + type args struct { + uid string + hash string + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "checkUpdateNeeded - no need to update", + fields: fields{ + Items: []hashItem{ + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120002", + Hash: "dabfadfd", + Active: true, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120012", + Hash: "dabfadf3", + Active: true, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac12022", + Hash: "dab6fadf", + Active: true, + }, + }, + }, + args: args{ + uid: "3c99c410-3cdd-11ee-be56-0242ac12022", + hash: "dab6fadf", + }, + want: false, + }, { + name: "checkUpdateNeeded - need to update", + fields: fields{ + Items: []hashItem{ + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120002", + Hash: "dabfadfd", + Active: true, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120012", + Hash: "dabfadf3", + Active: true, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac12022", + Hash: "dab6fadf", + Active: true, + }, + }, + }, + args: args{ + uid: "3c99c410-3cdd-11ee-be56-0242ac12022", + hash: "dab6faff", + }, + want: true, + }, { + name: "checkUpdateNeeded - new uid", + fields: fields{ + Items: []hashItem{ + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120002", + Hash: "dabfadfd", + Active: true, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120012", + Hash: "dabfadf3", + Active: true, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac12022", + Hash: "dab6fadf", + Active: true, + }, + }, + }, + args: args{ + uid: "3c99c410-3cdd-11ee-be56-0242ac12032", + hash: "dab6faff", + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rc := hashList{ + Items: tt.fields.Items, + } + if got := rc.checkUpdateNeeded(tt.args.uid, tt.args.hash); got != tt.want { + t.Errorf("hashList.checkUpdateNeeded() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_hashList_updateHash(t *testing.T) { + + type fields struct { + Items []hashItem + } + type args struct { + uid string + hash string + } + + testfields := fields{ + Items: []hashItem{ + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120002", + Hash: "1234", + Active: false, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120022", + Hash: "12345", + Active: false, + }, + }, + } + testfieldsupdated := fields{ + Items: []hashItem{ + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120002", + Hash: "1234", + Active: false, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120022", + Hash: "123467ff", + Active: false, + }, + }, + } + testfieldsupdated2 := fields{ + Items: []hashItem{ + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120002", + Hash: "1234", + Active: false, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120022", + Hash: "123467ff", + Active: false, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120032", + Hash: "123467", + Active: true, + }, + }, + } + + tests := []struct { + name string + fields fields + args args + want fields + }{ + { + name: "Test updateHash valid input", + fields: testfields, + args: args{ + uid: "3c99c410-3cdd-11ee-be56-0242ac120022", + hash: "123467ff", + }, + want: testfieldsupdated, + }, { + name: "Test updateHash add new hash", + fields: testfieldsupdated, + args: args{ + uid: "3c99c410-3cdd-11ee-be56-0242ac120032", + hash: "123467", + }, + want: testfieldsupdated2, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + hl := &hashList{ + Items: tt.fields.Items, + } + hl.updateHash(tt.args.uid, tt.args.hash) + assert.Equal(t, hl.Items, tt.want.Items) + }) + } +} diff --git a/internal/services/resourceupdatev2/resourcecache.go b/internal/services/resourceupdatev2/resourcecache.go new file mode 100644 index 0000000..edcbe86 --- /dev/null +++ b/internal/services/resourceupdatev2/resourcecache.go @@ -0,0 +1,101 @@ +package resourceupdatev2 + +import ( + "fmt" + "time" + + "github.com/NorskHelsenett/ror-agent/internal/services/authservice" + + "github.com/NorskHelsenett/ror/pkg/apicontracts/apiresourcecontracts" + + "github.com/NorskHelsenett/ror/pkg/rlog" + + "github.com/NorskHelsenett/ror/pkg/helpers/stringhelper" + + "github.com/go-co-op/gocron" +) + +var ResourceCache resourcecache + +type resourcecache struct { + HashList hashList + Workqueue ResourceCacheWorkqueue + cleanupRunning bool + scheduler *gocron.Scheduler +} + +func (rc *resourcecache) Init() error { + var err error + rc.HashList, err = getResourceHashList() + if err != nil { + return err + } + rc.scheduler = gocron.NewScheduler(time.Local) + rc.scheduler.StartAsync() + rc.addWorkqueScheduler(10) + rc.startCleanup() + return nil +} + +func (rc resourcecache) CleanupRunning() bool { + return rc.cleanupRunning +} +func (rc *resourcecache) MarkActive(uid string) { + rc.HashList.markActive(uid) +} + +func (rc resourcecache) addWorkqueScheduler(seconds int) { + _, _ = rc.scheduler.Every(seconds).Second().Tag("workquerunner").Do(rc.runWorkqueScheduler) +} +func (rc resourcecache) runWorkqueScheduler() { + if rc.Workqueue.NeedToRun() { + rlog.Warn("resourceQue has non zero lenght", rlog.Int("resource que length", rc.Workqueue.ItemCount())) + rc.RunWorkQue() + } +} + +func (rc *resourcecache) startCleanup() { + rc.cleanupRunning = true + _, _ = rc.scheduler.Every(1).Day().At(time.Now().Add(time.Minute * 1)).Tag("resourcescleanup").Do(rc.finnishCleanup) +} + +func (rc *resourcecache) finnishCleanup() { + if !rc.cleanupRunning { + return + } + rc.cleanupRunning = false + _ = rc.scheduler.RemoveByTag("resourcescleanup") + inactive := rc.HashList.getInactiveUid() + if len(inactive) == 0 { + return + } + for _, uid := range inactive { + rlog.Info(fmt.Sprintf("Removing resource %s", uid)) + resource := apiresourcecontracts.ResourceUpdateModel{ + Owner: authservice.CreateOwnerref(), + Uid: uid, + Action: apiresourcecontracts.K8sActionDelete, + } + _ = sendResourceUpdateToRor(&resource) + } + rlog.Info(fmt.Sprintf("resource cleanup done, %d resources removed", len(inactive))) +} + +func (rc resourcecache) PrettyPrintHashes() { + stringhelper.PrettyprintStruct(rc.HashList) +} + +// RunWorkQue Will run from the scheduler if the resource-que is non zero length. +// Resources in the que wil be requed using the sendResourceUpdateToRor function. +func (rc *resourcecache) RunWorkQue() { + for _, resourceReturn := range rc.Workqueue { + err := sendResourceUpdateToRor(resourceReturn.ResourceUpdate) + if err != nil { + rlog.Error("error re-sending resource update to ror, added to retryque", err) + rc.Workqueue.Add(resourceReturn.ResourceUpdate) + return + } + rc.Workqueue.DeleteByUid(resourceReturn.ResourceUpdate.Uid) + rc.HashList.updateHash(resourceReturn.ResourceUpdate.Uid, resourceReturn.ResourceUpdate.Hash) + } +} diff --git a/internal/services/resourceupdatev2/resourcecache_test.go b/internal/services/resourceupdatev2/resourcecache_test.go new file mode 100644 index 0000000..2248eeb --- /dev/null +++ b/internal/services/resourceupdatev2/resourcecache_test.go @@ -0,0 +1,186 @@ +package resourceupdatev2 + +import ( + "testing" + + "github.com/go-co-op/gocron" + "github.com/stretchr/testify/assert" +) + +func Test_resourcecache_CleanupRunning(t *testing.T) { + type fields struct { + HashList hashList + Workqueue ResourceCacheWorkqueue + cleanupRunning bool + scheduler *gocron.Scheduler + } + tests := []struct { + name string + fields fields + want bool + }{ + { + name: "Cleanup is running", + fields: fields{ + cleanupRunning: true, + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rc := resourcecache{ + HashList: tt.fields.HashList, + Workqueue: tt.fields.Workqueue, + cleanupRunning: tt.fields.cleanupRunning, + scheduler: tt.fields.scheduler, + } + if got := rc.CleanupRunning(); got != tt.want { + t.Errorf("resourcecache.CleanupRunning() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_resourcecache_MarkActive(t *testing.T) { + type fields struct { + HashList hashList + Workqueue ResourceCacheWorkqueue + cleanupRunning bool + scheduler *gocron.Scheduler + } + + testfields := fields{ + HashList: hashList{ + Items: []hashItem{ + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120002", + Hash: "1234", + Active: false, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120022", + Hash: "12345", + Active: false, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac130022", + Hash: "12346", + Active: false, + }, + }, + }, + Workqueue: ResourceCacheWorkqueue{}, + cleanupRunning: false, + } + + testfieldsupdated := fields{ + HashList: hashList{ + Items: []hashItem{ + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120002", + Hash: "1234", + Active: true, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120022", + Hash: "12345", + Active: false, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac130022", + Hash: "12346", + Active: false, + }, + }, + }, + Workqueue: ResourceCacheWorkqueue{}, + cleanupRunning: false, + } + type args struct { + uid string + } + tests := []struct { + name string + fields fields + args args + want fields + }{ + { + name: "Test MarkActive valid input", + fields: testfields, + args: args{ + uid: "3c99c410-3cdd-11ee-be56-0242ac120002", + }, + want: testfieldsupdated, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rc := resourcecache{ + HashList: tt.fields.HashList, + Workqueue: tt.fields.Workqueue, + cleanupRunning: tt.fields.cleanupRunning, + scheduler: tt.fields.scheduler, + } + rc.MarkActive(tt.args.uid) + assert.Equal(t, rc.HashList, tt.want.HashList) + }) + } +} + +func Test_resourcecache_runWorkqueScheduler(t *testing.T) { + type fields struct { + HashList hashList + Workqueue ResourceCacheWorkqueue + cleanupRunning bool + scheduler *gocron.Scheduler + } + testfields := fields{ + HashList: hashList{ + Items: []hashItem{ + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120002", + Hash: "1234", + Active: false, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120022", + Hash: "12345", + Active: false, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac130022", + Hash: "12346", + Active: false, + }, + }, + }, + Workqueue: ResourceCacheWorkqueue{}, + cleanupRunning: false, + } + + tests := []struct { + name string + fields fields + want fields + }{ + { + name: "Test runWorkqueScheduler empty workque", + fields: testfields, + want: testfields, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rc := resourcecache{ + HashList: tt.fields.HashList, + Workqueue: tt.fields.Workqueue, + cleanupRunning: tt.fields.cleanupRunning, + scheduler: tt.fields.scheduler, + } + rc.runWorkqueScheduler() + assert.Equal(t, rc.Workqueue, tt.want.Workqueue) + }) + } +} diff --git a/internal/services/resourceupdatev2/resourceupdate.go b/internal/services/resourceupdatev2/resourceupdate.go new file mode 100644 index 0000000..f6cf2f0 --- /dev/null +++ b/internal/services/resourceupdatev2/resourceupdate.go @@ -0,0 +1,44 @@ +package resourceupdatev2 + +import ( + "github.com/NorskHelsenett/ror-agent/internal/models/rorResources" + "github.com/NorskHelsenett/ror-agent/internal/services/authservice" + + "github.com/NorskHelsenett/ror/pkg/apicontracts/apiresourcecontracts" + + "github.com/NorskHelsenett/ror/pkg/rlog" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func SendResource(action apiresourcecontracts.ResourceAction, input *unstructured.Unstructured) { + resource, err := rorResources.NewFromUnstructured(input) + resourceReturn := resource.NewResourceUpdateModel(authservice.CreateOwnerref(), action) + if err != nil { + return + } + + if action != apiresourcecontracts.K8sActionDelete { + if ResourceCache.CleanupRunning() { + ResourceCache.MarkActive(resourceReturn.Uid) + } + needUpdate := ResourceCache.HashList.checkUpdateNeeded(resourceReturn.Uid, resourceReturn.Hash) + if needUpdate { + err = sendResourceUpdateToRor(resourceReturn) + if err != nil { + rlog.Error("error sending resource update to ror, added to retryque", err) + ResourceCache.Workqueue.Add(resourceReturn) + return + } + ResourceCache.HashList.updateHash(resourceReturn.Uid, resourceReturn.Hash) + } + } else if action == apiresourcecontracts.K8sActionDelete { + err := sendResourceUpdateToRor(resourceReturn) + if err != nil { + rlog.Error("error sending resource update to ror, added to retryque", err) + ResourceCache.Workqueue.Add(resourceReturn) + return + } + } + +} diff --git a/internal/services/resourceupdatev2/workqueue.go b/internal/services/resourceupdatev2/workqueue.go new file mode 100644 index 0000000..4957de9 --- /dev/null +++ b/internal/services/resourceupdatev2/workqueue.go @@ -0,0 +1,63 @@ +package resourceupdatev2 + +import ( + "time" + + "github.com/NorskHelsenett/ror/pkg/apicontracts/apiresourcecontracts" +) + +type ResourceCacheWorkqueueObject struct { + SubmittedTime time.Time + RetryCount int + ResourceUpdate *apiresourcecontracts.ResourceUpdateModel +} + +type ResourceCacheWorkqueue []ResourceCacheWorkqueueObject + +func (wq ResourceCacheWorkqueue) NeedToRun() bool { + return len(wq) > 0 +} +func (wq ResourceCacheWorkqueue) ItemCount() int { + return len(wq) +} + +func (m ResourceCacheWorkqueue) GetByUid(uid string) (ResourceCacheWorkqueueObject, int) { + for i, resourceUpdate := range m { + if resourceUpdate.ResourceUpdate.Uid == uid { + return resourceUpdate, i + } + } + var emptyResponse ResourceCacheWorkqueueObject + return emptyResponse, 0 +} +func (m *ResourceCacheWorkqueue) Add(resourceUpdate *apiresourcecontracts.ResourceUpdateModel) { + + resourceUpdateObject := ResourceCacheWorkqueueObject{ + SubmittedTime: time.Now(), + RetryCount: 0, + ResourceUpdate: resourceUpdate, + } + + cacheResource, currentId := m.GetByUid(resourceUpdate.Uid) + if currentId > 0 { + resourceUpdateObject.RetryCount = cacheResource.RetryCount + 1 + (*m)[currentId] = resourceUpdateObject + } else { + *m = append(*m, resourceUpdateObject) + } +} + +func (m *ResourceCacheWorkqueue) DeleteByUid(uid string) { + if uid == "" { + return + } + var newCache []ResourceCacheWorkqueueObject + + _, id := m.GetByUid(uid) + for i, resourceUpdateCacheObject := range *m { + if i != id { + newCache = append(newCache, resourceUpdateCacheObject) + } + } + *m = newCache +} diff --git a/internal/services/resourceupdatev2/workqueue_test.go b/internal/services/resourceupdatev2/workqueue_test.go new file mode 100644 index 0000000..33b27c4 --- /dev/null +++ b/internal/services/resourceupdatev2/workqueue_test.go @@ -0,0 +1,308 @@ +package resourceupdatev2 + +import ( + "reflect" + "testing" + "time" + + "github.com/NorskHelsenett/ror/pkg/apicontracts/apiresourcecontracts" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +func TestResourceCacheWorkqueue_ItemCount(t *testing.T) { + + workquevalues := []ResourceCacheWorkqueueObject{ + { + SubmittedTime: time.Now(), + RetryCount: 0, + ResourceUpdate: &apiresourcecontracts.ResourceUpdateModel{ + Uid: "3c99c410-3cdd-11ee-be56-0242ac120002", + }, + }, + { + SubmittedTime: time.Now().Add(time.Hour), + RetryCount: 0, + ResourceUpdate: &apiresourcecontracts.ResourceUpdateModel{ + Uid: "3c99c410-3cdd-11ee-be56-0242ac120012", + }, + }, + } + tests := []struct { + name string + wq ResourceCacheWorkqueue + want int + }{ + { + name: "Test ItemCount", + wq: workquevalues, + want: 2, + }, { + name: "Test ItemCount empty que", + wq: []ResourceCacheWorkqueueObject{}, + want: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.wq.ItemCount(); got != tt.want { + t.Errorf("ResourceCacheWorkqueue.ItemCount() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestResourceCacheWorkqueue_GetByUid(t *testing.T) { + type args struct { + uid string + } + + time1 := time.Now().Add(time.Hour) + time2 := time.Now().Add(time.Hour * 2) + workquevalues := []ResourceCacheWorkqueueObject{ + { + SubmittedTime: time1, + RetryCount: 0, + ResourceUpdate: &apiresourcecontracts.ResourceUpdateModel{ + Uid: "3c99c410-3cdd-11ee-be56-0242ac120002", + }, + }, + { + SubmittedTime: time2, + RetryCount: 0, + ResourceUpdate: &apiresourcecontracts.ResourceUpdateModel{ + Uid: "3c99c410-3cdd-11ee-be56-0242ac120012", + }, + }, + } + + tests := []struct { + name string + m ResourceCacheWorkqueue + args args + want ResourceCacheWorkqueueObject + want1 int + }{ + { + name: "Test GetByUid empty request", + m: workquevalues, + args: args{ + uid: "", + }, + want: ResourceCacheWorkqueueObject{}, + want1: 0, + }, { + name: "Test GetByUid", + m: workquevalues, + args: args{ + uid: "3c99c410-3cdd-11ee-be56-0242ac120012", + }, + want: ResourceCacheWorkqueueObject{ + SubmittedTime: time2, + RetryCount: 0, + ResourceUpdate: &apiresourcecontracts.ResourceUpdateModel{ + Uid: "3c99c410-3cdd-11ee-be56-0242ac120012", + }, + }, + want1: 1, + }, { + name: "Test GetByUid Nonexistent uid", + m: workquevalues, + args: args{ + uid: "3c99c410-3cdd-11ee-be56-0242ac120022", + }, + want: ResourceCacheWorkqueueObject{}, + want1: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1 := tt.m.GetByUid(tt.args.uid) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ResourceCacheWorkqueue.GetByUid() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("ResourceCacheWorkqueue.GetByUid() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + +func TestResourceCacheWorkqueue_Add(t *testing.T) { + type args struct { + resourceUpdate *apiresourcecontracts.ResourceUpdateModel + } + + testresourceUpdate := &apiresourcecontracts.ResourceUpdateModel{ + Uid: "3c99c410-3cdd-11ee-be56-0242ac120012", + } + time1 := time.Now().Add(time.Hour) + time2 := time.Now().Add(time.Hour * 2) + + var testworkque ResourceCacheWorkqueue = []ResourceCacheWorkqueueObject{ + { + SubmittedTime: time1, + RetryCount: 0, + ResourceUpdate: &apiresourcecontracts.ResourceUpdateModel{ + Uid: "3c99c410-3cdd-11ee-be56-0242ac120002", + }, + }, + { + SubmittedTime: time2, + RetryCount: 0, + ResourceUpdate: &apiresourcecontracts.ResourceUpdateModel{ + Uid: "3c99c410-3cdd-11ee-be56-0242ac120012", + }, + }, + } + + var testworkqueadded ResourceCacheWorkqueue = []ResourceCacheWorkqueueObject{ + { + SubmittedTime: time1, + RetryCount: 0, + ResourceUpdate: &apiresourcecontracts.ResourceUpdateModel{ + Uid: "3c99c410-3cdd-11ee-be56-0242ac120002", + }, + }, + { + SubmittedTime: time2, + RetryCount: 1, + ResourceUpdate: &apiresourcecontracts.ResourceUpdateModel{ + Uid: "3c99c410-3cdd-11ee-be56-0242ac120012", + }, + }, + } + var testworkqueadded2 ResourceCacheWorkqueue = []ResourceCacheWorkqueueObject{ + { + SubmittedTime: time1, + RetryCount: 0, + ResourceUpdate: &apiresourcecontracts.ResourceUpdateModel{ + Uid: "3c99c410-3cdd-11ee-be56-0242ac120002", + }, + }, + { + SubmittedTime: time2, + RetryCount: 1, + ResourceUpdate: &apiresourcecontracts.ResourceUpdateModel{ + Uid: "3c99c410-3cdd-11ee-be56-0242ac120012", + }, + }, { + SubmittedTime: time2, + RetryCount: 0, + ResourceUpdate: &apiresourcecontracts.ResourceUpdateModel{ + Uid: "3c99c410-3cdd-11ee-be56-0242ac120022", + }, + }, + } + tests := []struct { + name string + m *ResourceCacheWorkqueue + args args + want *ResourceCacheWorkqueue + }{ + { + name: "Test Add existing uid", + m: &testworkque, + args: args{ + resourceUpdate: testresourceUpdate, + }, + want: &testworkqueadded, + }, { + name: "Test Add new uid", + m: &testworkque, + args: args{ + resourceUpdate: &apiresourcecontracts.ResourceUpdateModel{ + Uid: "3c99c410-3cdd-11ee-be56-0242ac120022", + }, + }, + want: &testworkqueadded2, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.m.Add(tt.args.resourceUpdate) + opts := cmpopts.IgnoreTypes(time.Now()) + if !cmp.Equal(tt.m, tt.want, opts) { + t.Errorf("%s failed", tt.name) + } + }) + } +} + +func TestResourceCacheWorkqueue_DeleteByUid(t *testing.T) { + type args struct { + uid string + } + var testworkqueadded ResourceCacheWorkqueue = []ResourceCacheWorkqueueObject{ + { + SubmittedTime: time.Now(), + RetryCount: 1, + ResourceUpdate: &apiresourcecontracts.ResourceUpdateModel{ + Uid: "3c99c410-3cdd-11ee-be56-0242ac120012", + }, + }, + } + var testworkqueadded2 ResourceCacheWorkqueue = []ResourceCacheWorkqueueObject{ + { + SubmittedTime: time.Now(), + RetryCount: 1, + ResourceUpdate: &apiresourcecontracts.ResourceUpdateModel{ + Uid: "3c99c410-3cdd-11ee-be56-0242ac120012", + }, + }, { + SubmittedTime: time.Now(), + RetryCount: 0, + ResourceUpdate: &apiresourcecontracts.ResourceUpdateModel{ + Uid: "3c99c410-3cdd-11ee-be56-0242ac120022", + }, + }, + } + + var testworkqueempty ResourceCacheWorkqueue + + tests := []struct { + name string + m *ResourceCacheWorkqueue + args args + want *ResourceCacheWorkqueue + }{ + { + name: "Test Remove uid", + m: &testworkqueadded2, + args: args{ + uid: "3c99c410-3cdd-11ee-be56-0242ac120022", + }, + want: &testworkqueadded, + }, { + name: "Test Remove uid - empty uid", + m: &testworkqueadded, + args: args{ + uid: "", + }, + want: &testworkqueadded, + }, { + name: "Test Remove uid - remove last que item", + m: &testworkqueadded, + args: args{ + uid: "3c99c410-3cdd-11ee-be56-0242ac120012", + }, + want: &testworkqueempty, + }, { + name: "Test Remove uid - remove from empty que", + m: &testworkqueempty, + args: args{ + uid: "3c99c410-3cdd-11ee-be56-0242ac120012", + }, + want: &testworkqueempty, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.m.DeleteByUid(tt.args.uid) + if !cmp.Equal(tt.m, tt.want, cmpopts.IgnoreTypes(time.Now()), cmpopts.EquateEmpty()) { + t.Errorf("%s failed", tt.name) + } + }) + } +} diff --git a/internal/utils/ingress.go b/internal/utils/ingress.go new file mode 100644 index 0000000..4a293cb --- /dev/null +++ b/internal/utils/ingress.go @@ -0,0 +1,207 @@ +package utils + +import ( + "context" + "fmt" + "strings" + + "github.com/NorskHelsenett/ror/pkg/apicontracts" + + "github.com/NorskHelsenett/ror/pkg/rlog" + + networkingV1 "k8s.io/api/networking/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/utils/strings/slices" + "sigs.k8s.io/controller-runtime/pkg/client/config" +) + +func GetIngressDetails(ingress *networkingV1.Ingress) (*apicontracts.Ingress, error) { + var newIngress apicontracts.Ingress + ingressNameSpace := ingress.Namespace + ingressName := ingress.Name + ingressClassName := "" + + var rules []apicontracts.IngressRule + var health apicontracts.Health = 1 + + if ingress.Spec.IngressClassName != nil { + ingressClassName = *ingress.Spec.IngressClassName + } + + k8sConfig := config.GetConfigOrDie() + k8sClient, err := kubernetes.NewForConfig(k8sConfig) + if err != nil { + rlog.Error("error in config", err) + } + + if ingress.Spec.Rules == nil { + return nil, fmt.Errorf("invalid ingress - missing rules") + } + + for ruleindex, irule := range ingress.Spec.Rules { + rlog.Debug("rule for host", rlog.String("host", irule.Host)) + rules = append(rules, apicontracts.IngressRule{ + Hostname: irule.Host, + IPAddresses: nil, + Paths: nil, + }) + + if ingress.Status.LoadBalancer.Ingress == nil { + rlog.Debug("Ingress has no IP-address", rlog.String("ingress", ingress.Name)) + } else { + for _, is := range ingress.Status.LoadBalancer.Ingress { + if is.Hostname == irule.Host { + rules[ruleindex].IPAddresses = append(rules[ruleindex].IPAddresses, is.IP) + } + } + } + + for _, irulepath := range irule.IngressRuleValue.HTTP.Paths { + rlog.Debug("rule for path", rlog.String("path", irulepath.Path)) + rlog.Debug("", rlog.String("service", irulepath.Backend.Service.Name)) + service, err := GetIngressService(k8sClient, ingressNameSpace, irulepath.Backend.Service.Name) + if err != nil { + rules[ruleindex].Paths = append(rules[ruleindex].Paths, apicontracts.IngressPath{ + Path: irulepath.Path, + Service: apicontracts.Service{}, + }) + continue + } + rules[ruleindex].Paths = append(rules[ruleindex].Paths, apicontracts.IngressPath{ + Path: irulepath.Path, + Service: service, + }) + } + } + + newIngress = apicontracts.Ingress{ + UID: string(ingress.UID), + Health: health, + Name: ingressName, + Namespace: ingressNameSpace, + Class: ingressClassName, + Rules: rules, + } + + richIngress, err := GetIngressHealth(newIngress) + + return richIngress, nil + +} + +func GetIngressHealth(thisIngress apicontracts.Ingress) (*apicontracts.Ingress, error) { + + ingressClasses := []string{"internett", "helsenett", "datacenter"} + thisIngressClass := strings.Split(thisIngress.Class, "-")[len(strings.Split(thisIngress.Class, "-"))-1] + + if !slices.Contains(ingressClasses, thisIngressClass) { + thisIngress.Health = 3 + } + if len(thisIngress.Rules) < 1 { + thisIngress.Health = 3 + } else { + for _, rule := range thisIngress.Rules { + if len(rule.IPAddresses) < 1 { + thisIngress.Health = 3 + } + if len(rule.Paths) < 1 { + thisIngress.Health = 3 + } else { + for _, path := range rule.Paths { + if path.Service.Type != "NodePort" { + thisIngress.Health = 3 + } + if len(path.Service.Endpoints) < 0 { + thisIngress.Health = 3 + } + } + } + } + } + + return &thisIngress, nil + +} + +func GetIngressService(k8sClient *kubernetes.Clientset, namespace string, serviceName string) (apicontracts.Service, error) { + + var service apicontracts.Service + var endpoints []apicontracts.EndpointAddress + var ports []apicontracts.ServicePort + + listOptions := metav1.ListOptions{} + svcs, err := k8sClient.CoreV1().Services(namespace).List(context.TODO(), listOptions) + if err != nil { + rlog.Fatal("could not list svcs", err) + } + for _, svc := range svcs.Items { + if svc.Name == serviceName { + // if svc.Spec.Type != "NodePort" { + // health = 3 + // } + + for _, port := range svc.Spec.Ports { + ports = append(ports, apicontracts.ServicePort{ + Name: port.Name, + NodePort: fmt.Sprint(port.NodePort), + Protocol: string(port.Protocol), + }) + } + + service = apicontracts.Service{ + Name: serviceName, + Type: string(svc.Spec.Type), + Selector: svc.Spec.Selector["app.kubernetes.io/name"], + Ports: ports, + Endpoints: nil, + } + rlog.Debug("service added ", rlog.String("service", serviceName)) + } + } + + if service.Name == "" { + service = apicontracts.Service{ + Name: serviceName, + Type: "", + Selector: "", + Ports: nil, + Endpoints: nil, + } + rlog.Debug("Could not find Service", rlog.String("service name", serviceName)) + } + + eps, _ := k8sClient.CoreV1().Endpoints(namespace).List(context.TODO(), listOptions) + if err != nil { + rlog.Fatal("could not list eps", err) + } + for _, ep := range eps.Items { + if ep.Name != serviceName { + continue + } + + if ep.Subsets == nil { + continue + } + + for _, epAddress := range ep.Subsets[0].Addresses { + nodename := "None" + if epAddress.NodeName != nil { + nodename = *epAddress.NodeName + } + podname := "None" + if epAddress.TargetRef != nil { + podname = epAddress.TargetRef.Name + } + endpoints = append(endpoints, apicontracts.EndpointAddress{ + NodeName: nodename, + PodName: podname, + }) + service.Endpoints = endpoints + } + + } + + return service, nil + +} diff --git a/v2/Dockerfile b/v2/Dockerfile new file mode 100644 index 0000000..4a8da6e --- /dev/null +++ b/v2/Dockerfile @@ -0,0 +1,12 @@ +ARG GCR_MIRROR=gcr.io/ +FROM ${GCR_MIRROR}distroless/static:nonroot +LABEL org.opencontainers.image.source https://github.com/norskhelsenett/ror-agent +LABEL org.opencontainers.image.description ROR Agent v2 +WORKDIR / + +COPY dist/agent /bin/ror-cluster-agent +USER 10000:10000 +EXPOSE 8100 + +ENTRYPOINT ["/bin/ror-cluster-agent"] + diff --git a/v2/Dockerfile.compose b/v2/Dockerfile.compose new file mode 100644 index 0000000..c6d5b1b --- /dev/null +++ b/v2/Dockerfile.compose @@ -0,0 +1,15 @@ +ARG DOCKER_MIRROR=docker.io/ +ARG GCR_MIRROR=gcr.io/ +FROM ${DOCKER_MIRROR}golang:1.23-alpine as builder +WORKDIR /app +COPY . . +RUN go get ./... + +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o dist/ror-agent cmd/agent/main.go + +FROM ${GCR_MIRROR}distroless/static:nonroot +WORKDIR / +COPY --from=builder /app/dist/ror-agent . +USER 10000:10000 + +ENTRYPOINT ["/ror-agent"] diff --git a/v2/cmd/agent/main.go b/v2/cmd/agent/main.go new file mode 100644 index 0000000..2946081 --- /dev/null +++ b/v2/cmd/agent/main.go @@ -0,0 +1,62 @@ +package main + +import ( + "os" + "os/signal" + + "github.com/NorskHelsenett/ror-agent/v2/internal/agentconfig" + "github.com/NorskHelsenett/ror-agent/v2/internal/clients" + "github.com/NorskHelsenett/ror-agent/v2/internal/handlers/dynamicclienthandler" + "github.com/NorskHelsenett/ror-agent/v2/internal/scheduler" + "github.com/NorskHelsenett/ror-agent/v2/internal/services/resourceupdatev2" + "github.com/NorskHelsenett/ror-agent/v2/pkg/clients/dynamicclient" + + "github.com/NorskHelsenett/ror/pkg/config/configconsts" + "github.com/NorskHelsenett/ror/pkg/config/rorclientconfig" + + "github.com/NorskHelsenett/ror/pkg/rlog" + + "syscall" + + "github.com/spf13/viper" + + "go.uber.org/automaxprocs/maxprocs" +) + +func main() { + _ = "rebuild 12" + _, _ = maxprocs.Set(maxprocs.Logger(rlog.Infof)) + agentconfig.Init() + rlog.Info("Agent is starting", rlog.String("version", viper.GetString(configconsts.VERSION)), rlog.String("commit", viper.GetString(configconsts.COMMIT))) + sigs := make(chan os.Signal, 1) // Create channel to receive os signals + stop := make(chan struct{}) // Create channel to receive stop signal + signal.Notify(sigs, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) // Register the sigs channel to receieve SIGTERM + + clientConfig := rorclientconfig.ClientConfig{ + Role: viper.GetString(configconsts.ROLE), + Namespace: viper.GetString(configconsts.POD_NAMESPACE), + ApiKeySecret: viper.GetString(configconsts.API_KEY_SECRET), + ApiKey: viper.GetString(configconsts.API_KEY), + ApiEndpoint: viper.GetString(configconsts.API_ENDPOINT), + RorVersion: agentconfig.GetRorVersion(), + MustInitializeKubernetes: true, + } + + clients.InitClients(clientConfig) + + err := resourceupdatev2.ResourceCache.Init() + if err != nil { + rlog.Fatal("could not get hashlist for clusterid", err) + } + + dynamicclienthandler := dynamicclienthandler.NewDynamicClientHandler() + err = dynamicclient.Start(clients.Kubernetes, dynamicclienthandler, stop, sigs) + if err != nil { + rlog.Fatal("could not start dynamic client", err) + } + + scheduler.SetUpScheduler() + + <-stop + rlog.Info("Shutting down...") +} diff --git a/v2/go.mod b/v2/go.mod new file mode 100644 index 0000000..17c3b9e --- /dev/null +++ b/v2/go.mod @@ -0,0 +1,98 @@ +module github.com/NorskHelsenett/ror-agent/v2 + +go 1.23.1 + +require ( + github.com/NorskHelsenett/ror v0.3.11-rc3 + github.com/go-co-op/gocron v1.37.0 + github.com/go-resty/resty/v2 v2.16.0 + github.com/google/go-cmp v0.6.0 + github.com/spf13/viper v1.19.0 + github.com/stretchr/testify v1.9.0 + go.uber.org/automaxprocs v1.6.0 + k8s.io/apimachinery v0.31.2 + k8s.io/client-go v0.31.2 +) + +require ( + github.com/bytedance/sonic v1.12.4 // indirect + github.com/bytedance/sonic/loader v0.2.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.6 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gin-gonic/gin v1.10.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.4 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.23.0 // indirect + github.com/goccy/go-json v0.10.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/imdario/mergo v0.3.6 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.mongodb.org/mongo-driver v1.17.1 // indirect + go.opentelemetry.io/otel v1.32.0 // indirect + go.opentelemetry.io/otel/trace v1.32.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/arch v0.12.0 // indirect + golang.org/x/crypto v0.29.0 // indirect + golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect + golang.org/x/net v0.31.0 // indirect + golang.org/x/oauth2 v0.23.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/term v0.26.0 // indirect + golang.org/x/text v0.20.0 // indirect + golang.org/x/time v0.6.0 // indirect + google.golang.org/protobuf v1.35.2 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.31.2 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect + k8s.io/metrics v0.31.2 // indirect + k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect + sigs.k8s.io/controller-runtime v0.19.1 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/v2/go.sum b/v2/go.sum new file mode 100644 index 0000000..aa8aafa --- /dev/null +++ b/v2/go.sum @@ -0,0 +1,275 @@ +github.com/NorskHelsenett/ror v0.3.11-rc3 h1:IoP5CYIupD4HryeGUOjmJArYuOR7MfYavER4jkdVhQk= +github.com/NorskHelsenett/ror v0.3.11-rc3/go.mod h1:k3YOVf4W/DqJ/fKNSv4+m9H8ylTlQhAEyoU59bzpDP0= +github.com/bytedance/sonic v1.12.4 h1:9Csb3c9ZJhfUWeMtpCDCq6BUoH5ogfDFLUgQ/jG+R0k= +github.com/bytedance/sonic v1.12.4/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E= +github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc= +github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0= +github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= +github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-resty/resty/v2 v2.16.0 h1:qpKalHWI2bpp9BIKlyT8TYWEJXOk1NuKbfiT3RRnzWc= +github.com/go-resty/resty/v2 v2.16.0/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM= +go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4= +go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= +go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= +go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= +go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg= +golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= +golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.31.2 h1:3wLBbL5Uom/8Zy98GRPXpJ254nEFpl+hwndmk9RwmL0= +k8s.io/api v0.31.2/go.mod h1:bWmGvrGPssSK1ljmLzd3pwCQ9MgoTsRCuK35u6SygUk= +k8s.io/apimachinery v0.31.2 h1:i4vUt2hPK56W6mlT7Ry+AO8eEsyxMD1U44NR22CLTYw= +k8s.io/apimachinery v0.31.2/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/client-go v0.31.2 h1:Y2F4dxU5d3AQj+ybwSMqQnpZH9F30//1ObxOKlTI9yc= +k8s.io/client-go v0.31.2/go.mod h1:NPa74jSVR/+eez2dFsEIHNa+3o09vtNaWwWwb1qSxSs= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= +k8s.io/metrics v0.31.2 h1:sQhujR9m3HN/Nu/0fTfTscjnswQl0qkQAodEdGBS0N4= +k8s.io/metrics v0.31.2/go.mod h1:QqqyReApEWO1UEgXOSXiHCQod6yTxYctbAAQBWZkboU= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +sigs.k8s.io/controller-runtime v0.19.1 h1:Son+Q40+Be3QWb+niBXAg2vFiYWolDjjRfO8hn/cxOk= +sigs.k8s.io/controller-runtime v0.19.1/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/v2/internal/agentconfig/variables.go b/v2/internal/agentconfig/variables.go new file mode 100644 index 0000000..51d7664 --- /dev/null +++ b/v2/internal/agentconfig/variables.go @@ -0,0 +1,63 @@ +//This package contains the variables used by the agentconfig package +//It also contains the Init function which initializes the configuration +//and sets the default values for the configuration variables +//It also contains the IncreaseErrorCount and ResetErrorCount functions +//which are used to increase the error count and reset the error count respectively +//It also contains the GetRorVersion function which returns the RorVersion struct +//which contains the version and commit of the agent +// +// The version and commit are set at compile time using the ldflags +// The version and commit are set to the default v1.1.0/FFFFF if not set at compile time +// +// The configuration variables are set to the default values if not set in the environment +// The enviroment variables required are: +// ROLE default value is ClusterAgent +// HEALTH_ENDPOINT default value is :8100 +// POD_NAMESPACE default value is ror +// API_KEY_SECRET default value is ror-apikey +// API_KEY migt be provided by the user if a secret containg the api key is not present in the cluster +// + +package agentconfig + +import ( + "github.com/NorskHelsenett/ror/pkg/config/configconsts" + + "github.com/NorskHelsenett/ror/pkg/config/rorversion" + + "github.com/NorskHelsenett/ror/pkg/rlog" + + "github.com/spf13/viper" +) + +var ( + Version string = "1.1.0" + Commit string = "FFFFF" +) + +var ( + ErrorCount int +) + +func Init() { + rlog.InitializeRlog() + rlog.Info("Configuration initializing ...") + viper.SetDefault(configconsts.VERSION, Version) + viper.SetDefault(configconsts.ROLE, "ClusterAgent") + viper.SetDefault(configconsts.COMMIT, Commit) + viper.SetDefault(configconsts.HEALTH_ENDPOINT, ":8100") + viper.SetDefault(configconsts.POD_NAMESPACE, "ror") + viper.SetDefault(configconsts.API_KEY_SECRET, "ror-apikey") + viper.AutomaticEnv() +} + +func IncreaseErrorCount() { + ErrorCount++ +} +func ResetErrorCount() { + ErrorCount = 0 +} + +func GetRorVersion() rorversion.RorVersion { + return rorversion.NewRorVersion(viper.GetString(configconsts.VERSION), viper.GetString(configconsts.COMMIT)) +} diff --git a/v2/internal/clients/ror.go b/v2/internal/clients/ror.go new file mode 100644 index 0000000..509211b --- /dev/null +++ b/v2/internal/clients/ror.go @@ -0,0 +1,38 @@ +// The package implements clients for the ror-agent +package clients + +import ( + "fmt" + + kubernetesclient "github.com/NorskHelsenett/ror/pkg/clients/kubernetes" + "github.com/NorskHelsenett/ror/pkg/config/configconsts" + "github.com/NorskHelsenett/ror/pkg/config/rorclientconfig" + + "github.com/go-resty/resty/v2" + "github.com/spf13/viper" +) + +var Kubernetes *kubernetesclient.K8sClientsets +var RorConfig *rorclientconfig.RorClientConfig +var client *resty.Client + +// Deprecated: GetOrCreateRorClient is deprecated use rorconfig.GetRorClient() instead +func GetOrCreateRorClient() (*resty.Client, error) { + if client != nil { + return client, nil + } + + client = resty.New() + client.SetBaseURL(viper.GetString(configconsts.API_ENDPOINT)) + client.Header.Add("X-API-KEY", viper.GetString(configconsts.API_KEY)) + client.Header.Set("User-Agent", fmt.Sprintf("ROR-Agent/%s", viper.GetString(configconsts.VERSION))) + + return client, nil +} + +func InitClients(clientConfig rorclientconfig.ClientConfig) { + rorclientconfig.InitRorClientConfig(clientConfig) + RorConfig = rorclientconfig.RorConfig + Kubernetes = RorConfig.GetKubernetesClientSet() + viper.Set(configconsts.CLUSTER_ID, RorConfig.GetClusterId()) +} diff --git a/v2/internal/handlers/dynamicclienthandler/handlers.go b/v2/internal/handlers/dynamicclienthandler/handlers.go new file mode 100644 index 0000000..a9f885e --- /dev/null +++ b/v2/internal/handlers/dynamicclienthandler/handlers.go @@ -0,0 +1,34 @@ +package dynamicclienthandler + +import ( + "github.com/NorskHelsenett/ror-agent/v2/internal/services/resourceupdatev2" + "github.com/NorskHelsenett/ror-agent/v2/pkg/clients/dynamicclient" + + "github.com/NorskHelsenett/ror/pkg/rorresources/rortypes" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +type handler struct { +} + +func NewDynamicClientHandler() dynamicclient.DynamicClientHandler { + ret := handler{} + return &ret + +} + +func (handler) AddResource(obj any) { + rawData := obj.(*unstructured.Unstructured) + resourceupdatev2.SendResource(rortypes.K8sActionAdd, rawData) +} + +func (handler) DeleteResource(obj any) { + rawData := obj.(*unstructured.Unstructured) + resourceupdatev2.SendResource(rortypes.K8sActionDelete, rawData) +} + +func (handler) UpdateResource(_ any, obj any) { + rawData := obj.(*unstructured.Unstructured) + resourceupdatev2.SendResource(rortypes.K8sActionUpdate, rawData) +} diff --git a/v2/internal/handlers/dynamicclienthandler/schemas.go b/v2/internal/handlers/dynamicclienthandler/schemas.go new file mode 100644 index 0000000..ebd6cbd --- /dev/null +++ b/v2/internal/handlers/dynamicclienthandler/schemas.go @@ -0,0 +1,10 @@ +package dynamicclienthandler + +import ( + "github.com/NorskHelsenett/ror/pkg/rorresources/rordefs" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func (handler) GetSchemas() []schema.GroupVersionResource { + return rordefs.GetSchemasByType(rordefs.ApiResourceTypeAgent) +} diff --git a/v2/internal/scheduler/metrics.go b/v2/internal/scheduler/metrics.go new file mode 100644 index 0000000..94db1ad --- /dev/null +++ b/v2/internal/scheduler/metrics.go @@ -0,0 +1,199 @@ +package scheduler + +import ( + "context" + "encoding/json" + "time" + + "github.com/NorskHelsenett/ror-agent/v2/internal/agentconfig" + "github.com/NorskHelsenett/ror-agent/v2/internal/clients" + + "github.com/NorskHelsenett/ror/pkg/apicontracts" + "github.com/NorskHelsenett/ror/pkg/apicontracts/apiresourcecontracts" + + "github.com/NorskHelsenett/ror/pkg/rlog" + + apimachinery "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/client-go/kubernetes" +) + +func MetricsReporting() error { + k8sClient, err := clients.Kubernetes.GetKubernetesClientset() + if err != nil { + return err + } + var metricsReport apicontracts.MetricsReport + + metricsReportNodes, err := CreateNodeMetricsList(k8sClient) + if err != nil { + rlog.Error("error converting podmetrics", err) + return err + } + owner := clients.RorConfig.CreateOwnerref() + metricsReport.Owner = apiresourcecontracts.ResourceOwnerReference{ + Scope: owner.Scope, + Subject: string(owner.Subject), + } + metricsReport.Nodes = metricsReportNodes + + err = sendMetricsToRor(metricsReport) + + return err +} + +func sendMetricsToRor(metricsReport apicontracts.MetricsReport) error { + rorClient, err := clients.GetOrCreateRorClient() + if err != nil { + rlog.Error("Could not get ror-api client", err) + agentconfig.IncreaseErrorCount() + return err + } + + url := "/v1/metrics" + response, err := rorClient.R(). + SetHeader("Content-Type", "application/json"). + SetBody(metricsReport). + Post(url) + if err != nil { + rlog.Error("Could not send metrics data to ror-api", err) + agentconfig.IncreaseErrorCount() + return err + } + + if response == nil { + rlog.Error("Response is nil", err) + agentconfig.IncreaseErrorCount() + return err + } + + if !response.IsSuccess() { + agentconfig.IncreaseErrorCount() + rlog.Error("Got unsuccessful status code from ror-api", err, + rlog.Int("status code", response.StatusCode()), + rlog.Int("error count", agentconfig.ErrorCount)) + return err + } else { + agentconfig.ResetErrorCount() + rlog.Info("Metrics report sent to ror") + + byteReport, err := json.Marshal(metricsReport) + if err == nil { + rlog.Debug("", rlog.String("byte report", string(byteReport))) + } + } + return nil +} + +func CreateNodeMetricsList(k8sClient *kubernetes.Clientset) ([]apicontracts.NodeMetric, error) { + var nodeMetricsList apicontracts.NodeMetricsList + var metricsReportNodes []apicontracts.NodeMetric + + data, err := k8sClient.RESTClient().Get().AbsPath("apis/metrics.k8s.io/v1beta1/nodes").DoRaw(context.TODO()) + if err != nil { + rlog.Error("error converting podmetrics", err) + return metricsReportNodes, err + } + + err = json.Unmarshal(data, &nodeMetricsList) + if err != nil { + rlog.Error("error unmarshaling podmetrics", err) + return metricsReportNodes, err + } + + for _, node := range nodeMetricsList.Items { + + metricsReportNode, err := CreateNodeMetrics(node) + if err != nil { + rlog.Error("error converting podmetrics", err) + return metricsReportNodes, err + } + metricsReportNodes = append(metricsReportNodes, metricsReportNode) + } + + return metricsReportNodes, nil + +} + +func CreateNodeMetrics(node apicontracts.NodeMetricsListItem) (apicontracts.NodeMetric, error) { + var nodeMetric apicontracts.NodeMetric + var timestamp time.Time = node.Timestamp + + nodeCpuRaw, err := apimachinery.ParseQuantity(node.Usage.CPU) + if err != nil { + rlog.Error("error converting nodemetrics", err) + return nodeMetric, err + } + nodeCpu := nodeCpuRaw.MilliValue() + + nodeMemoryRaw, err := apimachinery.ParseQuantity(node.Usage.Memory) + if err != nil { + rlog.Error("error converting nodemetrics", err) + return nodeMetric, err + } + nodeMemory, _ := nodeMemoryRaw.AsInt64() + + nodeMetric = apicontracts.NodeMetric{ + Name: node.Metadata.Name, + TimeStamp: timestamp, + CpuUsage: nodeCpu, + MemoryUsage: nodeMemory, + } + return nodeMetric, nil +} +func CreatePodMetricsList(k8sClient *kubernetes.Clientset) ([]apicontracts.PodMetric, error) { + var podMetricsList apicontracts.PodMetricsList + var metricsReportPods []apicontracts.PodMetric + + data, err := k8sClient.RESTClient().Get().AbsPath("apis/metrics.k8s.io/v1beta1/pods").DoRaw(context.TODO()) + if err != nil { + rlog.Error("error unmarshaling podmetrics", err) + return metricsReportPods, err + } + + err = json.Unmarshal(data, &podMetricsList) + if err != nil { + rlog.Error("error unmarshaling podmetrics", err) + return metricsReportPods, err + } + + for _, pod := range podMetricsList.Items { + metricsReportPod, err := CreatePodMetrics(pod) + if err != nil { + rlog.Error("error converting podmetrics", err) + return metricsReportPods, err + } + metricsReportPods = append(metricsReportPods, metricsReportPod) + } + return metricsReportPods, nil +} + +func CreatePodMetrics(pod apicontracts.PodMetricsListItem) (apicontracts.PodMetric, error) { + var podMetric apicontracts.PodMetric + var timestamp time.Time = pod.Timestamp + var podCpuSum int64 = 0 + var podMemorySum int64 = 0 + + for _, container := range pod.Containers { + podCpu, err := apimachinery.ParseQuantity(container.Usage.CPU) + if err != nil { + rlog.Error("error converting podmetrics", err) + return podMetric, err + } + podCpuSum = podCpuSum + podCpu.MilliValue() + podMemoryObj, err := apimachinery.ParseQuantity(container.Usage.Memory) + if err != nil { + rlog.Error("error converting podmetrics", err) + return podMetric, err + } + podMemory, _ := podMemoryObj.AsInt64() + podMemorySum = podMemorySum + podMemory + } + podMetric = apicontracts.PodMetric{ + Name: pod.Metadata.Name, + Namespace: pod.Metadata.Namespace, + TimeStamp: timestamp, + CpuUsage: podCpuSum, + MemoryUsage: podMemorySum, + } + return podMetric, nil +} diff --git a/v2/internal/scheduler/setup.go b/v2/internal/scheduler/setup.go new file mode 100644 index 0000000..86262bc --- /dev/null +++ b/v2/internal/scheduler/setup.go @@ -0,0 +1,19 @@ +package scheduler + +import ( + "time" + + "github.com/NorskHelsenett/ror/pkg/rlog" + + "github.com/go-co-op/gocron" +) + +func SetUpScheduler() { + scheduler := gocron.NewScheduler(time.UTC) + _, err := scheduler.Every(1).Minute().Tag("metrics").Do(MetricsReporting) + if err != nil { + rlog.Fatal("Could not setup scheduler for metrics", err) + return + } + //scheduler.StartAsync() +} diff --git a/v2/internal/services/authservice/authservice.go b/v2/internal/services/authservice/authservice.go new file mode 100644 index 0000000..725e69e --- /dev/null +++ b/v2/internal/services/authservice/authservice.go @@ -0,0 +1,31 @@ +// TODO: Remove this file once the rorconfig is implemented +// authservice implements authorization helpers for the agent +package authservice + +import ( + "github.com/NorskHelsenett/ror/pkg/config/configconsts" + "github.com/NorskHelsenett/ror/pkg/rorresources/rortypes" + + aclmodels "github.com/NorskHelsenett/ror/pkg/models/acl" + + "github.com/NorskHelsenett/ror/pkg/apicontracts/apiresourcecontracts" + + "github.com/spf13/viper" +) + +// creaters a ownerref object for the agent +func CreateOwnerref() apiresourcecontracts.ResourceOwnerReference { + return apiresourcecontracts.ResourceOwnerReference{ + Scope: aclmodels.Acl2ScopeCluster, + Subject: viper.GetString(configconsts.CLUSTER_ID), + } + +} + +func CreateRorOwnerref() rortypes.RorResourceOwnerReference { + return rortypes.RorResourceOwnerReference{ + Scope: aclmodels.Acl2ScopeCluster, + Subject: aclmodels.Acl2Subject(viper.GetString(configconsts.CLUSTER_ID)), + } + +} diff --git a/v2/internal/services/initial_service.go b/v2/internal/services/initial_service.go new file mode 100644 index 0000000..7c7bfb3 --- /dev/null +++ b/v2/internal/services/initial_service.go @@ -0,0 +1,52 @@ +package services + +import ( + "fmt" + "io" + "net" + "net/http" + "strings" + + "github.com/NorskHelsenett/ror/pkg/rlog" +) + +var EgressIp string + +func GetEgressIp() { + internettCheck := "https://api.ipify.org/" + nhnCheck := "ip.nhn.no" + _, err := net.LookupIP(nhnCheck) + var apiHost string + if err != nil { + apiHost = internettCheck + } else { + apiHost = fmt.Sprintf("http://%s", nhnCheck) + } + + rlog.Info("Resolving ip", rlog.String("api host", apiHost)) + res, err := http.Get(apiHost) // #nosec G107 - we are not using user input + if err != nil { + // assuming retry but on internett + apiHost = internettCheck + res, err = http.Get(apiHost) // #nosec G107 - we are not using user input + if err != nil { + errorMsg := fmt.Sprintf("could not reach host %s", apiHost) + rlog.Info(errorMsg) + return + } + } + + body, err := io.ReadAll(res.Body) + _ = res.Body.Close() + if res.StatusCode > 299 { + rlog.Info("response failed", rlog.Int("status code", res.StatusCode), rlog.ByteString("body", body)) + return + } + + if err != nil { + rlog.Error("could not parse body", err) + return + } + + EgressIp = strings.Replace(string(body), "\n", "", -1) +} diff --git a/v2/internal/services/resourceupdatev2/client.go b/v2/internal/services/resourceupdatev2/client.go new file mode 100644 index 0000000..e5010b7 --- /dev/null +++ b/v2/internal/services/resourceupdatev2/client.go @@ -0,0 +1,70 @@ +package resourceupdatev2 + +import ( + "github.com/NorskHelsenett/ror-agent/v2/internal/agentconfig" + "github.com/NorskHelsenett/ror-agent/v2/internal/clients" + + "github.com/NorskHelsenett/ror/pkg/apicontracts/apiresourcecontracts" + + "github.com/NorskHelsenett/ror/pkg/rlog" + + "github.com/go-resty/resty/v2" +) + +// the function sends the resource to the ror api. If recieving a non 2xx statuscode it will retun an error. +func sendResourceUpdateToRor(resourceUpdate *apiresourcecontracts.ResourceUpdateModel) error { + rorClient, err := clients.GetOrCreateRorClient() + if err != nil { + rlog.Error("Could not get ror-api client", err) + agentconfig.IncreaseErrorCount() + return err + } + var url string + var response *resty.Response + + if resourceUpdate.Action == apiresourcecontracts.K8sActionAdd { + url = "/v1/resources" + response, err = rorClient.R(). + SetHeader("Content-Type", "application/json"). + SetBody(resourceUpdate). + Post(url) + + } else if resourceUpdate.Action == apiresourcecontracts.K8sActionUpdate { + url = "/v1/resources/uid/" + resourceUpdate.Uid + response, err = rorClient.R(). + SetHeader("Content-Type", "application/json"). + SetBody(resourceUpdate). + Put(url) + } else if resourceUpdate.Action == apiresourcecontracts.K8sActionDelete { + url = "/v1/resources/uid/" + resourceUpdate.Uid + response, err = rorClient.R(). + SetHeader("Content-Type", "application/json"). + SetBody(resourceUpdate). + Delete(url) + } + + if err != nil { + agentconfig.IncreaseErrorCount() + rlog.Error("could not send data to ror-api", err, + rlog.Int("error count", agentconfig.ErrorCount)) + return err + } + + if response == nil { + agentconfig.IncreaseErrorCount() + rlog.Error("response is nil", err, + rlog.Int("error count", agentconfig.ErrorCount)) + return err + } + + if !response.IsSuccess() { + agentconfig.IncreaseErrorCount() + rlog.Info("got non 200 statuscode from ror-api", rlog.Int("status code", response.StatusCode()), + rlog.Int("error count", agentconfig.ErrorCount)) + return err + } else { + agentconfig.ResetErrorCount() + rlog.Debug("partial update sent to ror", rlog.String("api verson", resourceUpdate.ApiVersion), rlog.String("kind", resourceUpdate.Kind), rlog.String("uid", resourceUpdate.Uid)) + } + return nil +} diff --git a/v2/internal/services/resourceupdatev2/docs.go b/v2/internal/services/resourceupdatev2/docs.go new file mode 100644 index 0000000..d3ac439 --- /dev/null +++ b/v2/internal/services/resourceupdatev2/docs.go @@ -0,0 +1,3 @@ +// package: resourceupdatev2 +// Path: cmd/agent/services/resourceupdatev2/resourceupdate.go +package resourceupdatev2 diff --git a/v2/internal/services/resourceupdatev2/hashlist.go b/v2/internal/services/resourceupdatev2/hashlist.go new file mode 100644 index 0000000..06bf9bd --- /dev/null +++ b/v2/internal/services/resourceupdatev2/hashlist.go @@ -0,0 +1,111 @@ +package resourceupdatev2 + +import ( + "fmt" + + "github.com/NorskHelsenett/ror-agent/v2/internal/clients" + "github.com/NorskHelsenett/ror-agent/v2/internal/services/authservice" + + "github.com/NorskHelsenett/ror/pkg/apicontracts/v2/apicontractsv2resources" + "github.com/NorskHelsenett/ror/pkg/rlog" +) + +// Hashlist for use in agent communication +type hashList struct { + Items []hashItem `json:"items"` +} + +// Item for use in the hashlist +type hashItem struct { + Uid string `json:"uid"` + Hash string `json:"hash"` + Active bool +} + +func InitHashList() (*apicontractsv2resources.HashList, error) { + var hashList apicontractsv2resources.HashList + rorclient := clients.RorConfig.GetRorClient() + apiHashList, err := rorclient.Resources().GetHashList(authservice.CreateRorOwnerref()) + if err != nil { + fmt.Println("Error getting hashlist from api", err) + return nil, err + } + for _, item := range apiHashList.Items { + hashList.Items = append(hashList.Items, apicontractsv2resources.HashItem{ + Uid: item.Uid, + Hash: item.Hash, + }) + } + return &hashList, nil + +} +func InitHashListv2() (*apicontractsv2resources.HashList, error) { + rorclient := clients.RorConfig.GetRorClient() + hashList, err := rorclient.ResourceV2().GetOwnHashes() + if err != nil { + fmt.Println("Error getting hashlist from api", err) + return nil, err + } + return hashList, nil + +} + +func (hl hashList) getInactiveUid() []string { + var ret []string + if len(hl.Items) == 0 { + return ret + } + for i := range hl.Items { + if !hl.Items[i].Active { + ret = append(ret, hl.Items[i].Uid) + } + } + return ret +} + +func (hl *hashList) markActive(uid string) { + item, i := hl.getHashByUid(uid) + if item.Uid != "" { + hl.Items[i].Active = true + } + +} + +// Returns a bool value of true if the resource need to be commited +func (rc hashList) checkUpdateNeeded(uid string, hash string) bool { + hashitem, _ := rc.getHashByUid(uid) + if hashitem.Hash == hash { + rlog.Debug("No need to update, hash matched") + return false + } else { + return true + } +} +func (hl hashList) getHashByUid(uid string) (hashItem, int) { + if len(hl.Items) > 0 { + for i := range hl.Items { + if hl.Items[i].Uid == uid { + return hl.Items[i], i + } + } + } + return hashItem{}, 0 +} + +// updates hash in internal hashlist on update. The api will update its list on commiting the resource to its database. +func (hl *hashList) updateHash(uid string, hash string) { + _, i := hl.getHashByUid(uid) + if i != 0 { + rlog.Debug("Update needed, hash updated", rlog.String("uid", uid)) + hl.Items[i].Hash = hash + return + } + rlog.Debug("Uid not found in hashList, adding hash", rlog.String("uid", uid)) + + newItem := hashItem{ + Uid: uid, + Hash: hash, + Active: true, + } + hl.Items = append(hl.Items, newItem) +} diff --git a/v2/internal/services/resourceupdatev2/hashlist_test.go b/v2/internal/services/resourceupdatev2/hashlist_test.go new file mode 100644 index 0000000..61f5a1e --- /dev/null +++ b/v2/internal/services/resourceupdatev2/hashlist_test.go @@ -0,0 +1,427 @@ +package resourceupdatev2 + +import ( + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stretchr/testify/assert" +) + +func Test_hashList_markActive(t *testing.T) { + + type fields struct { + Items []hashItem + } + type args struct { + uid string + } + + testfields := fields{ + Items: []hashItem{ + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120002", + Hash: "1234", + Active: false, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120022", + Hash: "12345", + Active: false, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac130022", + Hash: "12346", + Active: false, + }, + }, + } + testfieldsupdated := fields{ + Items: []hashItem{ + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120002", + Hash: "1234", + Active: true, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120022", + Hash: "12345", + Active: false, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac130022", + Hash: "12346", + Active: false, + }, + }, + } + + tests := []struct { + name string + fields fields + args args + want fields + }{ + { + name: "Test markActive valid input", + fields: testfields, + args: args{ + uid: "3c99c410-3cdd-11ee-be56-0242ac120002", + }, + want: testfieldsupdated, + }, { + name: "Test markActive on unknown uid", + fields: testfields, + args: args{ + uid: "3c99c410-3cdd-11ee-be56-0242ac120012", + }, + want: testfields, + }, { + name: "Test markActive on empty hashlist", + fields: fields{ + Items: []hashItem{}, + }, + args: args{ + uid: "3c99c410-3cdd-11ee-be56-0242ac120012", + }, + want: fields{ + Items: []hashItem{}, + }, + }, { + name: "Test markActive on empty uid", + fields: testfields, + args: args{ + uid: "", + }, + want: testfields, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + hl := &hashList{ + Items: tt.fields.Items, + } + hl.markActive(tt.args.uid) + assert.Equal(t, hl.Items, tt.want.Items) + }) + } +} + +func Test_hashList_getHashByUid(t *testing.T) { + type fields struct { + Items []hashItem + } + type args struct { + uid string + } + tests := []struct { + name string + fields fields + args args + want hashItem + want1 int + }{ + { + name: "Test getHashByUid, empty hashlist", + fields: fields{ + Items: []hashItem{}, + }, + args: args{ + uid: "3c99c410-3cdd-11ee-be56-0242ac120012", + }, + want: hashItem{}, + want1: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + hl := hashList{ + Items: tt.fields.Items, + } + got, got1 := hl.getHashByUid(tt.args.uid) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("hashList.getHashByUid() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("hashList.getHashByUid() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + +func Test_hashList_getInactiveUid(t *testing.T) { + type fields struct { + Items []hashItem + } + tests := []struct { + name string + fields fields + want []string + }{ + { + name: "getInactive", + fields: fields{ + Items: []hashItem{ + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120002", + Hash: "dabfadfd", + Active: false, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120012", + Hash: "dabfadf3", + Active: false, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac12022", + Hash: "dab6fadf", + Active: true, + }, + }, + }, + want: []string{ + "3c99c410-3cdd-11ee-be56-0242ac120002", + "3c99c410-3cdd-11ee-be56-0242ac120012", + }, + }, { + name: "getInactive - empty result", + fields: fields{ + Items: []hashItem{ + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120002", + Hash: "dabfadfd", + Active: true, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120012", + Hash: "dabfadf3", + Active: true, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac12022", + Hash: "dab6fadf", + Active: true, + }, + }, + }, + want: []string{}, + }, { + name: "getInactive - empty list", + fields: fields{ + Items: []hashItem{}, + }, + want: []string{}, + }, + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + hl := hashList{ + Items: tt.fields.Items, + } + if !cmp.Equal(hl.getInactiveUid(), tt.want, cmpopts.EquateEmpty()) { + t.Errorf("%s failed", tt.name) + } + }) + } +} + +func Test_hashList_checkUpdateNeeded(t *testing.T) { + type fields struct { + Items []hashItem + } + type args struct { + uid string + hash string + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "checkUpdateNeeded - no need to update", + fields: fields{ + Items: []hashItem{ + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120002", + Hash: "dabfadfd", + Active: true, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120012", + Hash: "dabfadf3", + Active: true, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac12022", + Hash: "dab6fadf", + Active: true, + }, + }, + }, + args: args{ + uid: "3c99c410-3cdd-11ee-be56-0242ac12022", + hash: "dab6fadf", + }, + want: false, + }, { + name: "checkUpdateNeeded - need to update", + fields: fields{ + Items: []hashItem{ + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120002", + Hash: "dabfadfd", + Active: true, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120012", + Hash: "dabfadf3", + Active: true, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac12022", + Hash: "dab6fadf", + Active: true, + }, + }, + }, + args: args{ + uid: "3c99c410-3cdd-11ee-be56-0242ac12022", + hash: "dab6faff", + }, + want: true, + }, { + name: "checkUpdateNeeded - new uid", + fields: fields{ + Items: []hashItem{ + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120002", + Hash: "dabfadfd", + Active: true, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120012", + Hash: "dabfadf3", + Active: true, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac12022", + Hash: "dab6fadf", + Active: true, + }, + }, + }, + args: args{ + uid: "3c99c410-3cdd-11ee-be56-0242ac12032", + hash: "dab6faff", + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rc := hashList{ + Items: tt.fields.Items, + } + if got := rc.checkUpdateNeeded(tt.args.uid, tt.args.hash); got != tt.want { + t.Errorf("hashList.checkUpdateNeeded() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_hashList_updateHash(t *testing.T) { + + type fields struct { + Items []hashItem + } + type args struct { + uid string + hash string + } + + testfields := fields{ + Items: []hashItem{ + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120002", + Hash: "1234", + Active: false, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120022", + Hash: "12345", + Active: false, + }, + }, + } + testfieldsupdated := fields{ + Items: []hashItem{ + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120002", + Hash: "1234", + Active: false, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120022", + Hash: "123467ff", + Active: false, + }, + }, + } + testfieldsupdated2 := fields{ + Items: []hashItem{ + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120002", + Hash: "1234", + Active: false, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120022", + Hash: "123467ff", + Active: false, + }, + { + Uid: "3c99c410-3cdd-11ee-be56-0242ac120032", + Hash: "123467", + Active: true, + }, + }, + } + + tests := []struct { + name string + fields fields + args args + want fields + }{ + { + name: "Test updateHash valid input", + fields: testfields, + args: args{ + uid: "3c99c410-3cdd-11ee-be56-0242ac120022", + hash: "123467ff", + }, + want: testfieldsupdated, + }, { + name: "Test updateHash add new hash", + fields: testfieldsupdated, + args: args{ + uid: "3c99c410-3cdd-11ee-be56-0242ac120032", + hash: "123467", + }, + want: testfieldsupdated2, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + hl := &hashList{ + Items: tt.fields.Items, + } + hl.updateHash(tt.args.uid, tt.args.hash) + assert.Equal(t, hl.Items, tt.want.Items) + }) + } +} diff --git a/v2/internal/services/resourceupdatev2/resourcecache.go b/v2/internal/services/resourceupdatev2/resourcecache.go new file mode 100644 index 0000000..8813e5b --- /dev/null +++ b/v2/internal/services/resourceupdatev2/resourcecache.go @@ -0,0 +1,123 @@ +package resourceupdatev2 + +import ( + "context" + "fmt" + "time" + + "github.com/NorskHelsenett/ror-agent/v2/internal/clients" + "github.com/NorskHelsenett/ror-agent/v2/internal/services/authservice" + + "github.com/NorskHelsenett/ror/pkg/apicontracts/apiresourcecontracts" + "github.com/NorskHelsenett/ror/pkg/apicontracts/v2/apicontractsv2resources" + + "github.com/NorskHelsenett/ror/pkg/rlog" + + "github.com/go-co-op/gocron" +) + +var ResourceCache resourcecache + +type resourcecache struct { + HashList *apicontractsv2resources.HashList + WorkQueue ResourceCacheWorkQueue + cleanupRunning bool + scheduler *gocron.Scheduler +} + +func (rc *resourcecache) Init() error { + var err error + rc.HashList, err = InitHashList() + if err != nil { + return err + } + rc.WorkQueue = NewResourceCacheWorkQueue() + rc.scheduler = gocron.NewScheduler(time.Local) + rc.addWorkQeueScheduler(10) + rc.scheduler.StartAsync() + rc.startCleanup() + return nil +} + +func (rc *resourcecache) CleanupRunning() bool { + return rc.cleanupRunning +} +func (rc *resourcecache) MarkActive(uid string) { + rc.HashList.MarkActive(uid) +} + +func (rc *resourcecache) addWorkQeueScheduler(seconds int) { + _, err := rc.scheduler.Every(seconds).Second().Tag("workQeuerunner").Do(rc.runWorkQeueScheduler) + if err != nil { + rlog.Error("error starting workQeueScheduler", err) + } + +} +func (rc *resourcecache) runWorkQeueScheduler() { + if rc.WorkQueue.NeedToRun() { + rlog.Warn("resourceQueue has non zero lenght", rlog.Int("resource Queue length", rc.WorkQueue.ItemCount())) + rc.RunWorkQeue() + } +} + +func (rc *resourcecache) startCleanup() { + rc.cleanupRunning = true + _, err := rc.scheduler.Every(1).Day().At(time.Now().Add(time.Minute * 1)).Tag("resourcescleanup").Do(rc.finnishCleanup) + if err != nil { + rlog.Error("error starting cleanup", err) + } +} + +func (rc *resourcecache) finnishCleanup() { + if !rc.cleanupRunning { + return + } + rc.cleanupRunning = false + _ = rc.scheduler.RemoveByTag("resourcescleanup") + inactive := rc.HashList.GetInactiveUid() + if len(inactive) == 0 { + return + } + for _, uid := range inactive { + rlog.Info(fmt.Sprintf("Removing resource %s", uid)) + + // rorres := rorkubernetes.NewResourceFromDynamicClient(input) + // err := rorres.SetRorMeta(rortypes.ResourceRorMeta{ + // Version: "v2", + // Ownerref: clients.RorConfig.CreateOwnerref(), + // Action: action, + // }) + + resource := apiresourcecontracts.ResourceUpdateModel{ + Owner: authservice.CreateOwnerref(), + Uid: uid, + Action: apiresourcecontracts.K8sActionDelete, + } + _ = sendResourceUpdateToRor(&resource) + } + rlog.Info(fmt.Sprintf("resource cleanup done, %d resources removed", len(inactive))) +} + +// RunWorkQueue Will run from the scheduler if the resource-Queue is non zero length. +// Resources in the Queue wil be reQeued using the sendResourceUpdateToRor function. +func (rc *resourcecache) RunWorkQeue() { + if !rc.WorkQueue.NeedToRun() { + return + } + cacheworkqueue := rc.WorkQueue.ConsumeWorkQeue() + rorclient := clients.RorConfig.GetRorClient() + status, err := rorclient.ResourceV2().Update(context.Background(), *cacheworkqueue.ResourceSet) + if err != nil { + rlog.Error("error sending resources update to ror, added to retryQeue", err) + rc.WorkQueue.reQueue(cacheworkqueue) + return + } + + failed := status.GetFailedResources() + if len(failed) > 0 { + for failuuid, result := range failed { + rlog.Error("error sending resource update to ror, added to retryQeue", fmt.Errorf("uid: %s, failed with status: %d message: %s", failuuid, result.Status, result.Message)) + rc.WorkQueue.reQueueResource(cacheworkqueue.GetByUid(failuuid), cacheworkqueue.GetRetrycount(failuuid)) + } + } +} diff --git a/v2/internal/services/resourceupdatev2/resourceupdate.go b/v2/internal/services/resourceupdatev2/resourceupdate.go new file mode 100644 index 0000000..d034887 --- /dev/null +++ b/v2/internal/services/resourceupdatev2/resourceupdate.go @@ -0,0 +1,45 @@ +package resourceupdatev2 + +import ( + "github.com/NorskHelsenett/ror-agent/v2/internal/clients" + + "github.com/NorskHelsenett/ror/pkg/rorresources/rorkubernetes" + + "github.com/NorskHelsenett/ror/pkg/rorresources/rortypes" + + "github.com/NorskHelsenett/ror/pkg/rlog" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func SendResource(action rortypes.ResourceAction, input *unstructured.Unstructured) { + rorres := rorkubernetes.NewResourceFromDynamicClient(input) + err := rorres.SetRorMeta(rortypes.ResourceRorMeta{ + Version: "v2", + Ownerref: clients.RorConfig.CreateOwnerref(), + Action: action, + }) + if err != nil { + rlog.Error("error setting rormeta", err) + return + } + + rorres.GenRorHash() + + if action != rortypes.K8sActionDelete && ResourceCache.CleanupRunning() { + ResourceCache.MarkActive(rorres.GetUID()) + } + + needUpdate := ResourceCache.HashList.CheckUpdateNeeded(rorres.GetUID(), rorres.GetRorHash()) + if needUpdate { + + ResourceCache.WorkQueue.Add(rorres) + // if err != nil { + // rlog.Error("error sending resource update to ror, added to retryQeue", err) + // ResourceCache.WorkQeueue.Add(rorres) + // return + // } + + } + +} diff --git a/v2/internal/services/resourceupdatev2/workqueue.go b/v2/internal/services/resourceupdatev2/workqueue.go new file mode 100644 index 0000000..99a9db7 --- /dev/null +++ b/v2/internal/services/resourceupdatev2/workqueue.go @@ -0,0 +1,134 @@ +package resourceupdatev2 + +import ( + "fmt" + "sync" + + "github.com/NorskHelsenett/ror/pkg/rorresources" + + "github.com/NorskHelsenett/ror/pkg/rlog" +) + +type ResourceCacheWorkQueue struct { + mu sync.RWMutex + *rorresources.ResourceSet + RetryCount map[string]int +} + +func NewResourceCacheWorkQueue() ResourceCacheWorkQueue { + return ResourceCacheWorkQueue{ResourceSet: rorresources.NewResourceSet(), RetryCount: make(map[string]int)} +} + +func (wq *ResourceCacheWorkQueue) GetRetrycount(uid string) int { + wq.mu.RLock() + defer wq.mu.RUnlock() + return wq.RetryCount[uid] +} + +func (wq *ResourceCacheWorkQueue) SetRetrycount(uid string, count int) { + wq.mu.Lock() + defer wq.mu.Unlock() + wq.RetryCount[uid] = count +} + +func (wq *ResourceCacheWorkQueue) AddResource(add *rorresources.Resource) { + wq.mu.Lock() + defer wq.mu.Unlock() + wq.Add(add) + ResourceCache.HashList.UpdateHash(add.GetUID(), add.GetRorHash()) +} + +func (wq *ResourceCacheWorkQueue) AddResourceSet(add *rorresources.ResourceSet) { + wq.mu.Lock() + defer wq.mu.Unlock() + for wq.Next() { + add.Add(wq.Get()) + } +} + +func (wq *ResourceCacheWorkQueue) reQueue(wqadd *ResourceCacheWorkQueue) { + faileduuids := "" + var failcount int + for wqadd.Next() { + resource := wqadd.Get() + retrycount := wqadd.RetryCount[resource.GetUID()] + 1 + if retrycount > 10 { + if faileduuids != "" { + faileduuids = fmt.Sprintf("%s, %s", faileduuids, resource.GetUID()) + } else { + faileduuids = resource.GetUID() + } + failcount++ + wq.DeleteByUid(resource.GetUID()) + } else { + wq.AddResource(resource) + wq.SetRetrycount(resource.GetUID(), retrycount) + } + } + if faileduuids != "" { + rlog.Error("retry limit reached", fmt.Errorf("%d resources has been retried 10 times, giving up", failcount), rlog.String("uuids", faileduuids)) + } + +} + +func (wq *ResourceCacheWorkQueue) reQueueResource(resource *rorresources.Resource, retrycount int) { + retrycount++ + if retrycount > 10 { + rlog.Error("retry limit reached", fmt.Errorf("resource has been retried 10 times, giving up"), rlog.String("uuids", resource.GetUID())) + wq.DeleteByUid(resource.GetUID()) + } else { + wq.AddResource(resource) + wq.SetRetrycount(resource.GetUID(), retrycount) + } +} + +func (wq *ResourceCacheWorkQueue) NeedToRun() bool { + return wq.ItemCount() > 0 +} +func (wq *ResourceCacheWorkQueue) ItemCount() int { + wq.mu.RLock() + defer wq.mu.RUnlock() + return wq.Len() +} + +func (wq *ResourceCacheWorkQueue) GetQuedResourceByUid(uid string) *rorresources.Resource { + wq.mu.RLock() + defer wq.mu.RUnlock() + if res := wq.GetByUid(uid); res != nil { + return res + } + return nil +} + +func (wq *ResourceCacheWorkQueue) DeleteByUid(uid string) { + wq.mu.Lock() + defer wq.mu.Unlock() + if uid == "" { + return + } + wq.ResourceSet.DeleteByUid(uid) + wq.RetryCount[uid] = 0 +} + +func (wq *ResourceCacheWorkQueue) ConsumeWorkQeue() *ResourceCacheWorkQueue { + wq.mu.Lock() + returnQueue := wq.DeepCopy() + wq.mu.Unlock() + for _, res := range returnQueue.Resources { + wq.DeleteByUid(res.GetUID()) + } + return returnQueue +} + +func (wq *ResourceCacheWorkQueue) DeepCopy() *ResourceCacheWorkQueue { + returnQueue := &ResourceCacheWorkQueue{ + ResourceSet: rorresources.NewResourceSet(), + RetryCount: make(map[string]int), + } + for wq.Next() { + r := wq.Get() + returnQueue.Add(r) + } + returnQueue.RetryCount = wq.RetryCount + return returnQueue +} diff --git a/v2/pkg/clients/dynamicclient/client.go b/v2/pkg/clients/dynamicclient/client.go new file mode 100644 index 0000000..2c90dc5 --- /dev/null +++ b/v2/pkg/clients/dynamicclient/client.go @@ -0,0 +1,85 @@ +package dynamicclient + +import ( + "fmt" + "os" + + kubernetesclient "github.com/NorskHelsenett/ror/pkg/clients/kubernetes" + + "github.com/NorskHelsenett/ror/pkg/rlog" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/discovery" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/dynamic/dynamicinformer" + "k8s.io/client-go/tools/cache" +) + +type DynamicClientHandler interface { + AddResource(obj any) + DeleteResource(obj any) + UpdateResource(_ any, obj any) + GetSchemas() []schema.GroupVersionResource +} + +func Start(k *kubernetesclient.K8sClientsets, dynamichandler DynamicClientHandler, stop chan struct{}, sigs chan os.Signal) error { + rlog.Info("Starting dynamic watchers") + discoveryClient, err := k.GetDiscoveryClient() + if err != nil { + rlog.Error("Could not initialize discovery client", err) + return err + } + dynamicClient, err := k.GetDynamicClient() + if err != nil { + rlog.Error("Could not initialize dynamic client", err) + return err + } + + schemas := dynamichandler.GetSchemas() + for _, schema := range schemas { + check, err := discovery.IsResourceEnabled(discoveryClient, schema) + if err != nil { + rlog.Error("Could not query resources from cluster", err) + } + if check { + controller := newDynamicWatcher(dynamichandler, dynamicClient, schema) + go func() { + controller.Run(stop) + }() + } else { + errmsg := fmt.Sprintf("Could not register resource %s", schema.Resource) + rlog.Info(errmsg) + } + } + return nil +} + +type DynamicWatcher struct { + dynInformer cache.SharedIndexInformer + client dynamic.Interface +} + +func (c *DynamicWatcher) Run(stop <-chan struct{}) { + // Execute go function + go c.dynInformer.Run(stop) +} + +// Function creates a new dynamic controller to listen for api-changes in provided GroupVersionResource +func newDynamicWatcher(dynamichandler DynamicClientHandler, client dynamic.Interface, resource schema.GroupVersionResource) *DynamicWatcher { + dynWatcher := &DynamicWatcher{} + dynInformer := dynamicinformer.NewDynamicSharedInformerFactory(client, 0) + informer := dynInformer.ForResource(resource).Informer() + + dynWatcher.client = client + dynWatcher.dynInformer = informer + + _, err := informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: dynamichandler.AddResource, + UpdateFunc: dynamichandler.UpdateResource, + DeleteFunc: dynamichandler.DeleteResource, + }) + if err != nil { + rlog.Error("Error adding event handler", err) + } + + return dynWatcher +}