├── .github
├── CODEOWNERS
└── workflows
│ ├── build.yml
│ ├── docker-push.yml
│ └── release.yml
├── .gitignore
├── .golangci.yml
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── MAINTAINERS.md
├── Makefile
├── README.md
├── api
└── swagger.yaml
├── cmd
└── clouddriver
│ └── clouddriver.go
├── configs
├── README.md
└── krakend.json
├── deployments
├── spin-clouddriver.yaml
├── spin-doctor-service.yaml
├── spin-doctor.yaml
├── spin-go-clouddriver.yaml
├── spin-router-service.yaml
└── spin-router.yaml
├── go.mod
├── go.sum
├── internal
├── api
│ ├── core
│ │ ├── applications.go
│ │ ├── applications_test.go
│ │ ├── artifacts.go
│ │ ├── artifacts_test.go
│ │ ├── controller.go
│ │ ├── core_suite_test.go
│ │ ├── core_test.go
│ │ ├── credentials.go
│ │ ├── credentials_test.go
│ │ ├── features.go
│ │ ├── instances.go
│ │ ├── instances_test.go
│ │ ├── kubernetes
│ │ │ ├── cleanup.go
│ │ │ ├── cleanup_test.go
│ │ │ ├── controller.go
│ │ │ ├── delete.go
│ │ │ ├── delete_test.go
│ │ │ ├── deploy.go
│ │ │ ├── deploy_test.go
│ │ │ ├── disable.go
│ │ │ ├── disable_test.go
│ │ │ ├── enable.go
│ │ │ ├── enable_test.go
│ │ │ ├── kubernetes_suite_test.go
│ │ │ ├── kubernetes_test.go
│ │ │ ├── ops.go
│ │ │ ├── patch.go
│ │ │ ├── patch_test.go
│ │ │ ├── restart.go
│ │ │ ├── restart_test.go
│ │ │ ├── rollback.go
│ │ │ ├── rollback_test.go
│ │ │ ├── runjob.go
│ │ │ ├── runjob_test.go
│ │ │ ├── scale.go
│ │ │ └── scale_test.go
│ │ ├── manifest.go
│ │ ├── manifest_test.go
│ │ ├── ok.go
│ │ ├── ok_test.go
│ │ ├── ops.go
│ │ ├── ops_test.go
│ │ ├── payload_test.go
│ │ ├── project.go
│ │ ├── project_test.go
│ │ ├── search.go
│ │ ├── search_test.go
│ │ ├── security_groups.go
│ │ ├── task.go
│ │ ├── task_test.go
│ │ └── test
│ │ │ ├── expected-git-repo-master-subpath.tar.gz
│ │ │ ├── expected-git-repo-master.tar.gz
│ │ │ ├── expected-git-repo-test.tar.gz
│ │ │ ├── git-repo-master.tar.gz
│ │ │ └── git-repo-test.tar.gz
│ ├── server.go
│ └── v1
│ │ ├── controller.go
│ │ ├── payload_test.go
│ │ ├── provider.go
│ │ ├── provider_test.go
│ │ ├── resource.go
│ │ ├── resource_test.go
│ │ ├── v1_suite_test.go
│ │ └── v1_test.go
├── artifact
│ ├── artifact_suite_test.go
│ ├── artifactfakes
│ │ └── fake_credentials_controller.go
│ ├── controller.go
│ ├── controller_test.go
│ └── test
│ │ ├── credentials
│ │ └── test-keyfile.json
│ │ ├── custom-object.json
│ │ ├── docker-image.json
│ │ ├── embedded-base64.json
│ │ ├── front50-pipeline-template.json
│ │ ├── gcs-object-credentials.json
│ │ ├── gcs-object.json
│ │ ├── git-repo-token.json
│ │ ├── git-repo.json
│ │ ├── github-enterprise-file.json
│ │ ├── github-file.json
│ │ ├── helm-chart-basic-auth.json
│ │ ├── helm-chart.json
│ │ ├── http-file.json
│ │ └── kubernetes.json
├── controller.go
├── controller_test.go
├── fiat
│ ├── client.go
│ ├── client_test.go
│ ├── fiat_suite_test.go
│ └── fiatfakes
│ │ └── fake_client.go
├── front50
│ ├── client.go
│ ├── client_test.go
│ ├── front50_suite_test.go
│ └── front50fakes
│ │ └── fake_client.go
├── helm
│ ├── client.go
│ ├── client_test.go
│ ├── helm_suite_test.go
│ └── helmfakes
│ │ └── fake_client.go
├── internal_suite_test.go
├── kubernetes
│ ├── annotation.go
│ ├── annotation_test.go
│ ├── artifact.go
│ ├── artifact_test.go
│ ├── cached
│ │ ├── disk
│ │ │ ├── cached_discovery.go
│ │ │ ├── cached_discovery_test.go
│ │ │ ├── disk_suite_test.go
│ │ │ ├── diskfakes
│ │ │ │ └── fake_cache_round_tripper.go
│ │ │ ├── round_tripper.go
│ │ │ └── round_tripper_test.go
│ │ └── memory
│ │ │ ├── cache.go
│ │ │ ├── cache_test.go
│ │ │ ├── memory_suite_test.go
│ │ │ ├── round_tripper.go
│ │ │ └── round_tripper_test.go
│ ├── client.go
│ ├── clientset.go
│ ├── cluster.go
│ ├── cluster_test.go
│ ├── controller.go
│ ├── controller_test.go
│ ├── custom_kind.go
│ ├── custom_kind_test.go
│ ├── daemonset.go
│ ├── daemonset_test.go
│ ├── deployment.go
│ ├── deployment_test.go
│ ├── filter.go
│ ├── filter_test.go
│ ├── horizontalpodautoscaler.go
│ ├── horizontalpodautoscaler_test.go
│ ├── job.go
│ ├── job_test.go
│ ├── kubernetes_suite_test.go
│ ├── kubernetesfakes
│ │ ├── fake_client.go
│ │ ├── fake_clientset.go
│ │ └── fake_controller.go
│ ├── label.go
│ ├── label_selector.go
│ ├── label_selector_test.go
│ ├── label_test.go
│ ├── manifest
│ │ └── status.go
│ ├── namespace.go
│ ├── namespace_test.go
│ ├── patcher
│ │ └── patcher.go
│ ├── pod.go
│ ├── pod_test.go
│ ├── provider.go
│ ├── provider_test.go
│ ├── replicaset.go
│ ├── replicaset_test.go
│ ├── resource.go
│ ├── sort.go
│ ├── sort_test.go
│ ├── statefulset.go
│ ├── statefulset_test.go
│ ├── status.go
│ ├── strategy.go
│ ├── strategy_test.go
│ ├── traffic.go
│ ├── traffic_test.go
│ ├── unstructured.go
│ ├── unstructured_test.go
│ ├── version.go
│ └── version_test.go
├── map.go
├── map_test.go
├── middleware
│ ├── auth.go
│ ├── auth_test.go
│ ├── cachecontrol.go
│ ├── cachecontrol_test.go
│ ├── controller.go
│ ├── error.go
│ ├── log.go
│ ├── middleware_suite_test.go
│ ├── task.go
│ ├── vary.go
│ └── vary_test.go
├── sql
│ ├── client.go
│ ├── client_test.go
│ ├── sql_suite_test.go
│ └── sqlfakes
│ │ └── fake_client.go
├── time.go
└── time_test.go
└── pkg
├── artifact.go
├── credentials.go
├── error.go
├── logger.go
├── permissions.go
└── task.go
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @alice485 @pjberry16 @guido9j
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches: [master]
7 | pull_request:
8 | branches: [master]
9 | workflow_call:
10 |
11 | jobs:
12 | build:
13 | name: build
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: Check out code into the Go module directory
17 | uses: actions/checkout@v4
18 |
19 | - name: Set up Go
20 | uses: actions/setup-go@v5
21 | with:
22 | go-version-file: go.mod
23 |
24 | - name: Get dependencies
25 | run: |
26 | go get -v -t -d ./...
27 | if [ -f Gopkg.toml ]; then
28 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
29 | dep ensure
30 | fi
31 |
32 | - name: Verify dependencies
33 | run: go mod verify
34 |
35 | - name: lint
36 | uses: golangci/golangci-lint-action@v6
37 | with:
38 | # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
39 | version: v1.58
40 |
41 | - name: Build
42 | run: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -v cmd/clouddriver/clouddriver.go
43 |
44 | - name: Test
45 | run: go test -v ./...
46 | env:
47 | GOOGLE_APPLICATION_CREDENTIALS: test/credentials/test-keyfile.json
48 |
49 | - uses: actions/upload-artifact@v4
50 | with:
51 | name: build
52 | path: clouddriver
53 |
--------------------------------------------------------------------------------
/.github/workflows/docker-push.yml:
--------------------------------------------------------------------------------
1 | name: Docker
2 | on:
3 | workflow_call:
4 |
5 | env:
6 | CONTAINER_REGISTRY: ghcr.io
7 | IMAGE_NAME: ${{ github.repository }}
8 |
9 | jobs:
10 | push:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 | with:
15 | fetch-depth: 0
16 |
17 | - uses: actions/download-artifact@v4
18 | with:
19 | name: build
20 |
21 | - name: Add executable permissions
22 | run: |
23 | chmod +x clouddriver
24 |
25 | - name: Set release info
26 | run: |
27 | event_type=${{ github.event.action }}
28 | release_version=$(echo ${{ github.ref_name }} | sed 's/v//g' | sed 's/+/-/g')
29 | echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
30 | if [[ $event_type == "released" ]]; then
31 | echo "TAGS=${{ env.CONTAINER_REGISTRY }}/${{ env.IMAGE_NAME }}:$release_version,${{ env.CONTAINER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest" >> $GITHUB_ENV
32 | else
33 | echo "TAGS=${{ env.CONTAINER_REGISTRY }}/${{ env.IMAGE_NAME }}:$release_version" >> $GITHUB_ENV
34 | fi
35 |
36 | - name: Login
37 | uses: docker/login-action@v3
38 | with:
39 | registry: ${{ env.CONTAINER_REGISTRY }}
40 | username: ${{ github.actor }}
41 | password: ${{ secrets.GITHUB_TOKEN }}
42 |
43 | - name: Set up QEMU
44 | uses: docker/setup-qemu-action@v3
45 |
46 | - name: Set up docker context for buildx
47 | id: buildx-context
48 | run: |
49 | docker context create builders
50 |
51 | - name: Set up docker buildx
52 | uses: docker/setup-buildx-action@v2
53 | with:
54 | endpoint: builders
55 | driver-opts: |
56 | image=moby/buildkit:master
57 |
58 | - name: Build and push
59 | id: docker_build
60 | uses: docker/build-push-action@v5
61 | with:
62 | context: .
63 | push: true
64 | tags: ${{ env.TAGS }}
65 | labels: |
66 | org.opencontainers.image.source=https://github.com/${{ env.IMAGE_NAME }}
67 | org.opencontainers.image.version=${{ env.RELEASE_VERSION }}
68 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | release:
5 | types: [prereleased, released]
6 |
7 | jobs:
8 | build:
9 | uses: ./.github/workflows/build.yml
10 |
11 | docker:
12 | needs: build
13 | uses: ./.github/workflows/docker-push.yml
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /clouddriver
2 | /test/
3 | *.coverprofile
4 | clouddriver.db
5 | .idea/
6 | .DS_Store
7 | vendor/
8 | .vscode/
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | run:
2 | concurrency: 8
3 | issues-exit-code: 2
4 | tests: false
5 | timeout: 200s
6 |
7 | output:
8 | formats:
9 | - format: colored-line-number
10 |
11 | issues:
12 | max-issues-per-linter: 0
13 | max-same-issues: 0
14 | exclude-rules:
15 | - path: _test\.go
16 | linters:
17 | - gochecknoinits
18 |
19 | linters:
20 | disable-all: true
21 | enable:
22 | - bodyclose
23 | - dogsled
24 | - exportloopref
25 | - gocritic
26 | - gofmt
27 | - goimports
28 | - gosimple
29 | - govet
30 | - ineffassign
31 | - misspell
32 | - nakedret
33 | - prealloc
34 | - typecheck
35 | - unconvert
36 | - unparam
37 | - unused
38 | - whitespace
39 | - wsl
40 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributions Welcome
2 |
3 | First off, thank you for considering contributing to Go Clouddriver!
4 |
5 | If you're just looking for quick feedback for an idea or proposal, feel free to open an
6 | [issue](https://github.com/homedepot/go-clouddriver/issues/new).
7 |
8 | Follow the [contribution workflow](#contribution-workflow) for submitting your
9 | changes to the Go Clouddriver codebase.
10 |
11 | ## Contribution Workflow
12 |
13 | The Go Clouddriver repository uses the “fork-and-pull” development model. Follow these steps if
14 | you want to merge your changes to the Go Clouddriver repository:
15 |
16 | 1. Within your fork of
17 | [Go Clouddriver](https://github.com/homedepot/go-clouddriver), create a
18 | branch for your contribution. Use a meaningful name.
19 | 2. Create your contribution, meeting all
20 | [contribution quality standards](#contribution-quality-standards)
21 | 3. [Create a pull request](https://help.github.com/articles/creating-a-pull-request-from-a-fork/)
22 | against the master branch of the Go Clouddriver repository in the Homedepot org. Make sure to set the base repository as
23 | `homedepot/go-clouddriver`.
24 | 4. Add a reviewer to your pull request. Work with your reviewer to address any comments and obtain an approval.
25 | To update your pull request amend existing commits whenever applicable and
26 | then push the new changes to your pull request branch.
27 | 5. Once the pull request is approved, one of the [maintainers](MAINTAINERS.md) will merge it.
28 |
29 | ## Contribution Quality Standards
30 |
31 | Your contribution needs to meet the following standards:
32 |
33 | - Separate each **logical change** into its own commit.
34 | - Add a descriptive message for each commit. Follow
35 | [commit message best practices](https://github.com/erlang/otp/wiki/writing-good-commit-messages).
36 | - Document your pull requests. Include the reasoning behind each change, and
37 | the testing done.
38 | - Acknowledge the [Apache 2.0 license](LICENSE).
39 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine
2 | RUN apk add --no-cache ca-certificates curl
3 | COPY clouddriver /usr/local/bin
4 | CMD ["/usr/local/bin/clouddriver"]
5 |
--------------------------------------------------------------------------------
/MAINTAINERS.md:
--------------------------------------------------------------------------------
1 | # Maintainers
2 | This document contains a list of maintainers of this repo. For information on contributing see [CONTRIBUTING.md](CONTRIBUTING.md).
3 |
4 | | Name | GitHub |
5 | |------|--------|
6 | | James Guido | [@guido9j](https://github.com/guido9j)
|
7 | | Parag Patel | [@iteaguy](https://github.com/iteaguy)
|
8 | | Alice Garcia | [@alice485](https://github.com/alice485)
|
9 | | Don Benjamin | [@theassyrian](https://github.com/theassyrian)
|
10 | | Patrick Berry | [@pjberry16](https://github.com/pjberry16)
|
11 | | Ryan Johnson | [@ryanjohnsontv](https://github.com/ryanjohnsontv)
|
12 | | Abel Rodriguez | [@rodrig67](https://github.com/rodrig67)
|
13 | | Rodrigo Tovar | [@tovar-rodrigo](https://github.com/tovar-rodrigo)
|
14 | | Jose Anguiano | [@JoseAngui](https://github.com/JoseAngui)
|
15 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | default: all
2 |
3 | all: clean lint build test
4 |
5 | build:
6 | go build cmd/clouddriver/clouddriver.go
7 |
8 | clean:
9 | go clean
10 | -rm ./clouddriver
11 |
12 | lint:
13 | golangci-lint run
14 |
15 | run: clean lint build test
16 | ./clouddriver
17 |
18 | test:
19 | ginkgo -r
20 |
21 | tools:
22 | go get github.com/onsi/ginkgo/v2/ginkgo
23 | go get github.com/onsi/gomega/...
24 |
25 | vendor:
26 | go mod vendor
27 |
28 | .PHONY: all clean build lint run test tools
29 |
--------------------------------------------------------------------------------
/configs/README.md:
--------------------------------------------------------------------------------
1 | # configs
2 |
3 | KrakenD configuration is stored here. It currently supports the following providers. Any additional providers will follow the fork & pull model for contributions to this repository.
4 |
5 | | Provider | Supported |
6 | |----------|:-------------:|
7 | | Kubernetes | ✔️ |
8 | | Appengine | ✔️ |
9 |
--------------------------------------------------------------------------------
/deployments/spin-clouddriver.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | labels:
5 | app: spin
6 | cluster: spin-go-clouddriver
7 | name: spin-go-clouddriver
8 | namespace: spinnaker
9 | spec:
10 | ports:
11 | - port: 7002
12 | protocol: TCP
13 | targetPort: 7002
14 | selector:
15 | app: spin
16 | cluster: spin-go-clouddriver
17 | sessionAffinity: None
18 | type: ClusterIP
19 |
20 |
--------------------------------------------------------------------------------
/deployments/spin-doctor-service.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | labels:
5 | app: spin-doctor
6 | name: spin-doctor
7 | namespace: spinnaker
8 | spec:
9 | ports:
10 | - name: http
11 | port: 7002
12 | protocol: TCP
13 | targetPort: http
14 | selector:
15 | app: spin-doctor
16 |
--------------------------------------------------------------------------------
/deployments/spin-doctor.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | labels:
5 | app: spin-doctor
6 | name: spin-doctor
7 | namespace: spinnaker
8 | spec:
9 | progressDeadlineSeconds: 600
10 | replicas: 2
11 | revisionHistoryLimit: 10
12 | selector:
13 | matchLabels:
14 | app: spin-doctor
15 | strategy:
16 | rollingUpdate:
17 | maxSurge: 25%
18 | maxUnavailable: 25%
19 | type: RollingUpdate
20 | template:
21 | metadata:
22 | labels:
23 | app: spin-doctor
24 | spec:
25 | containers:
26 | - args:
27 | - -d
28 | - run
29 | - -c
30 | - /vault/secrets/krakend.json
31 | image: devopsfaith/krakend:1.4.1
32 | imagePullPolicy: IfNotPresent
33 | livenessProbe:
34 | failureThreshold: 3
35 | httpGet:
36 | path: /healthz
37 | port: http
38 | scheme: HTTP
39 | initialDelaySeconds: 15
40 | periodSeconds: 20
41 | successThreshold: 1
42 | timeoutSeconds: 1
43 | name: spin-doctor
44 | ports:
45 | - containerPort: 8000
46 | name: http
47 | protocol: TCP
48 | - containerPort: 9091
49 | name: metrics
50 | protocol: TCP
51 | readinessProbe:
52 | failureThreshold: 3
53 | httpGet:
54 | path: /healthz
55 | port: http
56 | scheme: HTTP
57 | initialDelaySeconds: 5
58 | periodSeconds: 10
59 | successThreshold: 1
60 | timeoutSeconds: 1
61 | resources:
62 | requests:
63 | cpu: "3"
64 | memory: 4Gi
65 |
--------------------------------------------------------------------------------
/deployments/spin-go-clouddriver.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | creationTimestamp: "2021-11-15T19:54:11Z"
5 | generation: 35
6 | labels:
7 | app: spin
8 | cluster: spin-go-clouddriver
9 | name: spin-go-clouddriver
10 | namespace: spinnaker
11 | spec:
12 | progressDeadlineSeconds: 600
13 | replicas: 2
14 | revisionHistoryLimit: 10
15 | selector:
16 | matchLabels:
17 | app: spin
18 | cluster: spin-go-clouddriver
19 | strategy:
20 | rollingUpdate:
21 | maxSurge: 25%
22 | maxUnavailable: 25%
23 | type: RollingUpdate
24 | template:
25 | metadata:
26 | labels:
27 | app: spin
28 | cluster: spin-go-clouddriver
29 | spec:
30 | containers:
31 | - args:
32 | - clouddriver
33 | command:
34 | - /bin/sh
35 | - -c
36 | env:
37 | - name: ARCADE_API_KEY
38 | value:
39 | - name: ARTIFACTS_CREDENTIALS_CONFIG_DIR
40 | value:
41 | - name: DB_HOST
42 | value:
43 | - name: DB_NAME
44 | value:
45 | - name: DB_PASS
46 | value:
47 | - name: DB_USER
48 | value:
49 | - name: GIN_MODE
50 | value: release
51 | - name: KUBERNETES_USE_DISK_CACHE
52 | value:
53 | - name: VERBOSE_REQUEST_LOGGING
54 | value:
55 | image: docker.io/oshomedepot/go-clouddriver:1.0.0
56 | name: clouddriver
57 | ports:
58 | - containerPort: 7002
59 | protocol: TCP
60 | readinessProbe:
61 | exec:
62 | command:
63 | - wget
64 | - --no-check-certificate
65 | - --spider
66 | - -q
67 | - http://localhost:7002/health
68 | failureThreshold: 3
69 | periodSeconds: 10
70 | successThreshold: 1
71 | timeoutSeconds: 1
72 | resources:
73 | requests:
74 | cpu: "3"
75 | memory: 4Gi
76 | volumeMounts:
77 | - mountPath: /var/kube/cache
78 | name: kube-cache-volume
79 | - args:
80 | - arcade
81 | command:
82 | - /bin/sh
83 | - -c
84 | env:
85 | - name: ARCADE_API_KEY
86 | value:
87 | image: docker.io/oshomedepot/arcade:1.0.2
88 | name: arcade
89 | ports:
90 | - containerPort: 1982
91 | protocol: TCP
92 | volumeMounts:
93 | - mountPath: /secret/arcade/providers
94 | name: arcade-providers-volume
95 | readOnly: true
96 | terminationGracePeriodSeconds: 720
97 | volumes:
98 | - name: arcade-providers-volume
99 | secret:
100 | defaultMode: 420
101 | secretName: arcade-providers
102 | - emptyDir: {}
103 | name: kube-cache-volume
104 |
105 |
--------------------------------------------------------------------------------
/deployments/spin-router-service.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | labels:
5 | app: spin-router
6 | name: spin-router
7 | namespace: spinnaker
8 | spec:
9 | ports:
10 | - nodePort: 31493
11 | port: 3000
12 | protocol: TCP
13 | targetPort: 3000
14 | selector:
15 | app: spin-router
16 |
--------------------------------------------------------------------------------
/deployments/spin-router.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | labels:
5 | app: spin-router
6 | name: spin-router
7 | namespace: spinnaker
8 | spec:
9 | progressDeadlineSeconds: 600
10 | replicas: 2
11 | revisionHistoryLimit: 10
12 | selector:
13 | matchLabels:
14 | app: spin-router
15 | strategy:
16 | rollingUpdate:
17 | maxSurge: 25%
18 | maxUnavailable: 25%
19 | type: RollingUpdate
20 | template:
21 | metadata:
22 | labels:
23 | app: spin-router
24 | spec:
25 | containers:
26 | - image: billiford/spin-router:v0.8.0
27 | livenessProbe:
28 | failureThreshold: 3
29 | httpGet:
30 | path: /healthz
31 | port: 3000
32 | scheme: HTTP
33 | initialDelaySeconds: 15
34 | periodSeconds: 20
35 | successThreshold: 1
36 | timeoutSeconds: 1
37 | name: spin-router
38 | ports:
39 | - containerPort: 3000
40 | protocol: TCP
41 | readinessProbe:
42 | failureThreshold: 3
43 | httpGet:
44 | path: /healthz
45 | port: 3000
46 | scheme: HTTP
47 | initialDelaySeconds: 5
48 | periodSeconds: 10
49 | successThreshold: 1
50 | timeoutSeconds: 1
51 | resources:
52 | requests:
53 | cpu: 100m
54 | memory: 256Mi
55 | terminationGracePeriodSeconds: 30
56 |
--------------------------------------------------------------------------------
/internal/api/core/controller.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "github.com/homedepot/go-clouddriver/internal"
5 | )
6 |
7 | // Controller holds all non request-scoped objects.
8 | type Controller struct {
9 | *internal.Controller
10 | }
11 |
--------------------------------------------------------------------------------
/internal/api/core/core_suite_test.go:
--------------------------------------------------------------------------------
1 | package core_test
2 |
3 | import (
4 | "testing"
5 |
6 | . "github.com/onsi/ginkgo/v2"
7 | . "github.com/onsi/gomega"
8 | )
9 |
10 | func TestCore(t *testing.T) {
11 | RegisterFailHandler(Fail)
12 | RunSpecs(t, "Core Suite")
13 | }
14 |
--------------------------------------------------------------------------------
/internal/api/core/features.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/gin-gonic/gin"
7 | )
8 |
9 | type Stages []Stage
10 |
11 | type Stage struct {
12 | Enabled bool `json:"enabled"`
13 | Name string `json:"name"`
14 | }
15 |
16 | var stages = []string{
17 | "resizeServerGroup",
18 | "runJob",
19 | "undoRolloutManifest",
20 | "rollingRestartManifest",
21 | "pauseRolloutManifest",
22 | "enableManifest",
23 | "scaleManifest",
24 | "disableManifest",
25 | "patchManifest",
26 | "resumeRolloutManifest",
27 | "deleteManifest",
28 | "deployManifest",
29 | "cleanupArtifacts",
30 | "upsertLoadBalancer",
31 | "enableServerGroup",
32 | "createServerGroup",
33 | "deleteLoadBalancer",
34 | "upsertScalingPolicy",
35 | "terminateInstances",
36 | "stopServerGroup",
37 | "disableServerGroup",
38 | "startServerGroup",
39 | "destroyServerGroup",
40 | }
41 |
42 | // Expected response:
43 | //
44 | // [
45 | //
46 | // {
47 | // "enabled": true,
48 | // "name": "resizeServerGroup"
49 | // },
50 | // {
51 | // "enabled": true,
52 | // "name": "runJob"
53 | // },
54 | // {
55 | // "enabled": true,
56 | // "name": "undoRolloutManifest"
57 | // },
58 | // {
59 | // "enabled": true,
60 | // "name": "rollingRestartManifest"
61 | // },
62 | // {
63 | // "enabled": true,
64 | // "name": "pauseRolloutManifest"
65 | // },
66 | // {
67 | // "enabled": true,
68 | // "name": "enableManifest"
69 | // },
70 | // {
71 | // "enabled": true,
72 | // "name": "scaleManifest"
73 | // },
74 | // {
75 | // "enabled": true,
76 | // "name": "disableManifest"
77 | // },
78 | // {
79 | // "enabled": true,
80 | // "name": "patchManifest"
81 | // },
82 | // {
83 | // "enabled": true,
84 | // "name": "resumeRolloutManifest"
85 | // },
86 | // {
87 | // "enabled": true,
88 | // "name": "deleteManifest"
89 | // },
90 | // {
91 | // "enabled": true,
92 | // "name": "deployManifest"
93 | // },
94 | // {
95 | // "enabled": true,
96 | // "name": "cleanupArtifacts"
97 | // },
98 | // {
99 | // "enabled": true,
100 | // "name": "upsertLoadBalancer"
101 | // },
102 | // {
103 | // "enabled": true,
104 | // "name": "enableServerGroup"
105 | // },
106 | // {
107 | // "enabled": true,
108 | // "name": "createServerGroup"
109 | // },
110 | // {
111 | // "enabled": true,
112 | // "name": "deleteLoadBalancer"
113 | // },
114 | // {
115 | // "enabled": true,
116 | // "name": "upsertScalingPolicy"
117 | // },
118 | // {
119 | // "enabled": true,
120 | // "name": "terminateInstances"
121 | // },
122 | // {
123 | // "enabled": true,
124 | // "name": "stopServerGroup"
125 | // },
126 | // {
127 | // "enabled": true,
128 | // "name": "disableServerGroup"
129 | // },
130 | // {
131 | // "enabled": true,
132 | // "name": "startServerGroup"
133 | // },
134 | // {
135 | // "enabled": true,
136 | // "name": "destroyServerGroup"
137 | // }
138 | //
139 | // ]
140 | func ListStages(c *gin.Context) {
141 | response := Stages{}
142 |
143 | for _, stage := range stages {
144 | s := Stage{
145 | Enabled: false,
146 | Name: stage,
147 | }
148 | response = append(response, s)
149 | }
150 |
151 | c.JSON(http.StatusOK, response)
152 | }
153 |
--------------------------------------------------------------------------------
/internal/api/core/kubernetes/cleanup.go:
--------------------------------------------------------------------------------
1 | package kubernetes
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "sort"
7 |
8 | "github.com/gin-gonic/gin"
9 | "github.com/google/uuid"
10 | "github.com/homedepot/go-clouddriver/internal"
11 | "github.com/homedepot/go-clouddriver/internal/kubernetes"
12 | clouddriver "github.com/homedepot/go-clouddriver/pkg"
13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
15 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
16 | )
17 |
18 | func (cc *Controller) CleanupArtifacts(c *gin.Context, ca CleanupArtifactsRequest) {
19 | app := c.GetHeader("X-Spinnaker-Application")
20 | taskID := clouddriver.TaskIDFromContext(c)
21 |
22 | for _, manifest := range ca.Manifests {
23 | u, err := kubernetes.ToUnstructured(manifest)
24 | if err != nil {
25 | clouddriver.Error(c, http.StatusBadRequest, err)
26 | return
27 | }
28 |
29 | provider, err := cc.KubernetesProvider(ca.Account)
30 | if err != nil {
31 | clouddriver.Error(c, http.StatusBadRequest, err)
32 | return
33 | }
34 |
35 | gvr, err := provider.Client.GVRForKind(u.GetKind())
36 | if err != nil {
37 | clouddriver.Error(c, http.StatusInternalServerError, err)
38 | return
39 | }
40 |
41 | namespace := u.GetNamespace()
42 |
43 | // Preserve backwards compatibility
44 | if len(provider.Namespaces) == 1 {
45 | namespace = provider.Namespaces[0]
46 | }
47 |
48 | err = provider.ValidateNamespaceAccess(namespace)
49 | if err != nil {
50 | clouddriver.Log(err)
51 | continue
52 | }
53 |
54 | // Grab the cluster of this resource from its annotations.
55 | cluster := clusterAnnotation(u)
56 | // Handle max version history. Source code here:
57 | // https://github.com/spinnaker/clouddriver/blob/master/clouddriver-kubernetes/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/op/artifact/KubernetesCleanupArtifactsOperation.java#L102
58 | maxVersionHistory, err := kubernetes.MaxVersionHistory(u)
59 | if err == nil && maxVersionHistory > 0 && cluster != "" {
60 | // Only list resources that are managed by Spinnaker.
61 | lo := metav1.ListOptions{
62 | LabelSelector: kubernetes.DefaultLabelSelector(),
63 | }
64 |
65 | ul, err := provider.Client.ListResourcesByKindAndNamespace(u.GetKind(), namespace, lo)
66 | if err != nil {
67 | clouddriver.Error(c, http.StatusInternalServerError,
68 | fmt.Errorf("error listing resources to cleanup for max version history (kind: %s, name: %s, namespace: %s): %v",
69 | u.GetKind(), u.GetName(), namespace, err))
70 |
71 | return
72 | }
73 |
74 | artifacts := kubernetes.FilterOnAnnotation(ul.Items,
75 | kubernetes.AnnotationSpinnakerMonikerCluster, cluster)
76 | if maxVersionHistory < len(artifacts) {
77 | // Sort on creation timestamp oldest to newest.
78 | sort.Slice(artifacts, func(i, j int) bool {
79 | return artifacts[i].GetCreationTimestamp().String() < artifacts[j].GetCreationTimestamp().String()
80 | })
81 |
82 | artifactsToDelete := artifacts[0 : len(artifacts)-maxVersionHistory]
83 | for _, a := range artifactsToDelete {
84 | // Delete the resource and any dependants in the foreground.
85 | pp := v1.DeletePropagationForeground
86 | do := metav1.DeleteOptions{
87 | PropagationPolicy: &pp,
88 | }
89 |
90 | err = provider.Client.DeleteResourceByKindAndNameAndNamespace(a.GetKind(), a.GetName(), namespace, do)
91 | if err != nil {
92 | clouddriver.Error(c, http.StatusInternalServerError,
93 | fmt.Errorf("error deleting resource to cleanup for max version history (kind: %s, name: %s, namespace: %s): %v",
94 | a.GetKind(), a.GetName(), namespace, err))
95 |
96 | return
97 | }
98 | }
99 | }
100 | }
101 |
102 | kr := kubernetes.Resource{
103 | AccountName: ca.Account,
104 | ID: uuid.New().String(),
105 | TaskID: taskID,
106 | TaskType: clouddriver.TaskTypeCleanup,
107 | Timestamp: internal.CurrentTimeUTC(),
108 | APIGroup: gvr.Group,
109 | Name: u.GetName(),
110 | Namespace: namespace,
111 | Resource: gvr.Resource,
112 | Version: gvr.Version,
113 | Kind: u.GetKind(),
114 | SpinnakerApp: app,
115 | Cluster: cluster,
116 | }
117 |
118 | err = cc.SQLClient.CreateKubernetesResource(kr)
119 | if err != nil {
120 | clouddriver.Error(c, http.StatusInternalServerError, err)
121 | return
122 | }
123 | }
124 | }
125 |
126 | // clusterAnnotation returns the value of the annotation
127 | // 'moniker.spinnaker.io/cluster' if it exists, otherwise
128 | // it returns an empty string.
129 | func clusterAnnotation(u unstructured.Unstructured) string {
130 | cluster := ""
131 |
132 | annotations := u.GetAnnotations()
133 | if annotations != nil {
134 | if value, ok := annotations[kubernetes.AnnotationSpinnakerMonikerCluster]; ok {
135 | cluster = value
136 | }
137 | }
138 |
139 | return cluster
140 | }
141 |
--------------------------------------------------------------------------------
/internal/api/core/kubernetes/controller.go:
--------------------------------------------------------------------------------
1 | package kubernetes
2 |
3 | import "github.com/homedepot/go-clouddriver/internal"
4 |
5 | // Controller holds all non request-scoped objects.
6 | type Controller struct {
7 | *internal.Controller
8 | }
9 |
--------------------------------------------------------------------------------
/internal/api/core/kubernetes/enable.go:
--------------------------------------------------------------------------------
1 | package kubernetes
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net/http"
7 | "strings"
8 | "time"
9 |
10 | "github.com/gin-gonic/gin"
11 | "github.com/google/uuid"
12 | "github.com/homedepot/go-clouddriver/internal"
13 | "github.com/homedepot/go-clouddriver/internal/kubernetes"
14 | clouddriver "github.com/homedepot/go-clouddriver/pkg"
15 | k8serrors "k8s.io/apimachinery/pkg/api/errors"
16 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
17 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
18 | )
19 |
20 | // Enable takes in manifest coordinates and grabs the list of load balancers behind which
21 | // it needs to be fronted from the annotation `traffic.spinnaker.io/load-balancers`.
22 | // It loops through these load balancers, adding any selectors from the load balancer's
23 | // labels and patching the target resource using the JSON patch strategy. It then
24 | // patches the labels of all pods that this manifest owns.
25 | func (cc *Controller) Enable(c *gin.Context, dm EnableManifestRequest) {
26 | taskID := clouddriver.TaskIDFromContext(c)
27 | namespace := dm.Location
28 |
29 | provider, err := cc.KubernetesProvider(dm.Account)
30 | if err != nil {
31 | clouddriver.Error(c, http.StatusBadRequest, err)
32 | return
33 | }
34 |
35 | // Preserve backwards compatibility
36 | if len(provider.Namespaces) == 1 {
37 | namespace = provider.Namespaces[0]
38 | }
39 |
40 | // ManifestName is the kind and name of the manifest, including any version, like
41 | // 'ReplicaSet test-rs-v001'.
42 | a := strings.Split(dm.ManifestName, " ")
43 | if len(a) != 2 {
44 | clouddriver.Error(c, http.StatusBadRequest, errInvalidManifestName)
45 | return
46 | }
47 |
48 | kind := a[0]
49 | name := a[1]
50 |
51 | err = provider.ValidateKindStatus(kind)
52 | if err != nil {
53 | clouddriver.Error(c, http.StatusBadRequest, err)
54 | return
55 | }
56 |
57 | err = provider.ValidateNamespaceAccess(namespace)
58 | if err != nil {
59 | clouddriver.Error(c, http.StatusBadRequest, err)
60 | return
61 | }
62 |
63 | // Grab the target manifest.
64 | target, err := provider.Client.Get(kind, name, namespace)
65 | if err != nil {
66 | if k8serrors.IsNotFound(err) {
67 | clouddriver.Error(c, http.StatusNotFound, fmt.Errorf("resource %s %s does not exist", kind, name))
68 | return
69 | }
70 |
71 | clouddriver.Error(c, http.StatusInternalServerError, fmt.Errorf("error getting resource (kind: %s, name: %s, namespace: %s): %v",
72 | kind, name, namespace, err))
73 |
74 | return
75 | }
76 |
77 | loadBalancers, err := kubernetes.LoadBalancers(*target)
78 | if err != nil {
79 | clouddriver.Error(c, http.StatusBadRequest, err)
80 | return
81 | }
82 |
83 | var pods []*unstructured.Unstructured
84 | // If the target manifest has load balancers and pods, list pods, grab those that has the owner UID
85 | // of the target manifest, and patch those pods.
86 | if len(loadBalancers) > 0 && hasPods(target) {
87 | // Declare server side filtering options.
88 | lo := metav1.ListOptions{
89 | FieldSelector: "metadata.namespace=" + namespace,
90 | LabelSelector: kubernetes.DefaultLabelSelector(),
91 | }
92 | // Declare a context with timeout.
93 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*internal.DefaultListTimeoutSeconds)
94 | defer cancel()
95 | // List resources with the context.
96 | ul, err := provider.Client.ListResourceWithContext(ctx, "pods", lo)
97 | if err != nil {
98 | clouddriver.Error(c, http.StatusInternalServerError, err)
99 | return
100 | }
101 | // Loop through all pods, finding all that are owned by the target manifest.
102 | for _, u := range ul.Items {
103 | for _, ownerReference := range u.GetOwnerReferences() {
104 | if ownerReference.UID == target.GetUID() {
105 | // Create a copy of the unstructured object since we access by reference.
106 | u := u
107 | pods = append(pods, &u)
108 | }
109 | }
110 | }
111 | }
112 |
113 | for _, loadBalancer := range loadBalancers {
114 | lb, err := getLoadBalancer(provider.Client, loadBalancer, namespace)
115 | if err != nil {
116 | clouddriver.Error(c, http.StatusInternalServerError, err)
117 | return
118 | }
119 |
120 | err = attachDetach(provider.Client, lb, target, "add")
121 | if err != nil {
122 | clouddriver.Error(c, http.StatusInternalServerError, err)
123 | return
124 | }
125 |
126 | // Patch all pods.
127 | for _, pod := range pods {
128 | err = attachDetach(provider.Client, lb, pod, "add")
129 | if err != nil {
130 | clouddriver.Error(c, http.StatusInternalServerError, err)
131 | return
132 | }
133 | }
134 | }
135 |
136 | // Just create one entry for a successful attachment to load balancers.
137 | kr := kubernetes.Resource{
138 | TaskType: clouddriver.TaskTypeNoOp,
139 | AccountName: dm.Account,
140 | SpinnakerApp: dm.App,
141 | ID: uuid.New().String(),
142 | TaskID: taskID,
143 | Name: name,
144 | Namespace: namespace,
145 | Kind: kind,
146 | }
147 |
148 | err = cc.SQLClient.CreateKubernetesResource(kr)
149 | if err != nil {
150 | clouddriver.Error(c, http.StatusInternalServerError, err)
151 | return
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/internal/api/core/kubernetes/kubernetes_suite_test.go:
--------------------------------------------------------------------------------
1 | package kubernetes_test
2 |
3 | import (
4 | "testing"
5 |
6 | . "github.com/onsi/ginkgo/v2"
7 | . "github.com/onsi/gomega"
8 | )
9 |
10 | func TestKubernetes(t *testing.T) {
11 | RegisterFailHandler(Fail)
12 | RunSpecs(t, "Kubernetes HTTP Suite")
13 | }
14 |
--------------------------------------------------------------------------------
/internal/api/core/kubernetes/patch.go:
--------------------------------------------------------------------------------
1 | package kubernetes
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "net/http"
7 | "strings"
8 |
9 | "github.com/gin-gonic/gin"
10 | "github.com/google/uuid"
11 | "github.com/homedepot/go-clouddriver/internal/kubernetes"
12 | clouddriver "github.com/homedepot/go-clouddriver/pkg"
13 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
14 | "k8s.io/apimachinery/pkg/types"
15 | )
16 |
17 | func (cc *Controller) Patch(c *gin.Context, pm PatchManifestRequest) {
18 | taskID := clouddriver.TaskIDFromContext(c)
19 | namespace := pm.Location
20 |
21 | provider, err := cc.KubernetesProvider(pm.Account)
22 | if err != nil {
23 | clouddriver.Error(c, http.StatusBadRequest, err)
24 | return
25 | }
26 |
27 | // Preserve backwards compatibility
28 | if len(provider.Namespaces) == 1 {
29 | namespace = provider.Namespaces[0]
30 | }
31 |
32 | b, err := json.Marshal(pm.PatchBody)
33 | if err != nil {
34 | clouddriver.Error(c, http.StatusBadRequest, err)
35 | return
36 | }
37 |
38 | // Manifest name is *really* the Spinnaker cluster - i.e. "deployment test-deployment", so we
39 | // need to split on a whitespace and get the actual name of the manifest.
40 | kind := ""
41 | name := pm.ManifestName
42 |
43 | a := strings.Split(pm.ManifestName, " ")
44 | if len(a) > 1 {
45 | kind = a[0]
46 | name = a[1]
47 | }
48 |
49 | err = provider.ValidateKindStatus(kind)
50 | if err != nil {
51 | clouddriver.Error(c, http.StatusBadRequest, err)
52 | return
53 | }
54 |
55 | err = provider.ValidateNamespaceAccess(namespace)
56 | if err != nil {
57 | clouddriver.Error(c, http.StatusBadRequest, err)
58 | return
59 | }
60 |
61 | // Only bind artifacts for "strategic" or "merge" strategy.
62 | //
63 | // See https://spinnaker.io/docs/guides/user/kubernetes-v2/patch-manifest/#override-artifacts
64 | if pm.Options.MergeStrategy == "strategic" ||
65 | pm.Options.MergeStrategy == "merge" {
66 | m := map[string]interface{}{}
67 | if err := json.Unmarshal(b, &m); err != nil {
68 | clouddriver.Error(c, http.StatusBadRequest, err)
69 | return
70 | }
71 |
72 | u := unstructured.Unstructured{
73 | Object: m,
74 | }
75 | kubernetes.BindArtifacts(&u, pm.AllArtifacts, pm.Account)
76 |
77 | b, err = json.Marshal(&u.Object)
78 | if err != nil {
79 | clouddriver.Error(c, http.StatusInternalServerError, err)
80 | return
81 | }
82 | }
83 |
84 | // Merge strategy can be "strategic", "json", or "merge".
85 | var strategy types.PatchType
86 |
87 | switch pm.Options.MergeStrategy {
88 | case "strategic":
89 | strategy = types.StrategicMergePatchType
90 | case "json":
91 | strategy = types.JSONPatchType
92 | case "merge":
93 | strategy = types.MergePatchType
94 | default:
95 | clouddriver.Error(c, http.StatusBadRequest,
96 | fmt.Errorf("invalid merge strategy %s", pm.Options.MergeStrategy))
97 | return
98 | }
99 |
100 | meta, _, err := provider.Client.PatchUsingStrategy(kind, name, namespace, b, strategy)
101 | if err != nil {
102 | clouddriver.Error(c, http.StatusInternalServerError, err)
103 | return
104 | }
105 |
106 | // TODO Record the applied patch in the kubernetes.io/change-cause annotation. If the annotation already exists, the contents are replaced.
107 | // if pm.Options.Record {
108 | // }
109 |
110 | kr := kubernetes.Resource{
111 | AccountName: pm.Account,
112 | ID: uuid.New().String(),
113 | TaskID: taskID,
114 | APIGroup: meta.Group,
115 | Name: meta.Name,
116 | Namespace: meta.Namespace,
117 | Resource: meta.Resource,
118 | Version: meta.Version,
119 | Kind: meta.Kind,
120 | SpinnakerApp: pm.App,
121 | }
122 |
123 | err = cc.SQLClient.CreateKubernetesResource(kr)
124 | if err != nil {
125 | clouddriver.Error(c, http.StatusInternalServerError, err)
126 | return
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/internal/api/core/kubernetes/restart.go:
--------------------------------------------------------------------------------
1 | package kubernetes
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "strings"
7 | "time"
8 |
9 | "github.com/gin-gonic/gin"
10 | "github.com/google/uuid"
11 | "github.com/homedepot/go-clouddriver/internal/kubernetes"
12 | clouddriver "github.com/homedepot/go-clouddriver/pkg"
13 | )
14 |
15 | // RollingRestart performs a `kubectl rollout restart` by setting an annotation on a pod template
16 | // to the current time in RFC3339.
17 | func (cc *Controller) RollingRestart(c *gin.Context, rr RollingRestartManifestRequest) {
18 | app := c.GetHeader("X-Spinnaker-Application")
19 | taskID := clouddriver.TaskIDFromContext(c)
20 | namespace := rr.Location
21 |
22 | provider, err := cc.KubernetesProvider(rr.Account)
23 | if err != nil {
24 | clouddriver.Error(c, http.StatusBadRequest, err)
25 | return
26 | }
27 |
28 | // Preserve backwards compatibility
29 | if len(provider.Namespaces) == 1 {
30 | namespace = provider.Namespaces[0]
31 | }
32 |
33 | a := strings.Split(rr.ManifestName, " ")
34 | kind := a[0]
35 | name := a[1]
36 |
37 | err = provider.ValidateKindStatus(kind)
38 | if err != nil {
39 | clouddriver.Error(c, http.StatusBadRequest, err)
40 | return
41 | }
42 |
43 | err = provider.ValidateNamespaceAccess(namespace)
44 | if err != nil {
45 | clouddriver.Error(c, http.StatusBadRequest, err)
46 | return
47 | }
48 |
49 | u, err := provider.Client.Get(kind, name, namespace)
50 | if err != nil {
51 | clouddriver.Error(c, http.StatusInternalServerError, err)
52 | return
53 | }
54 |
55 | var meta kubernetes.Metadata
56 |
57 | switch strings.ToLower(kind) {
58 | case "deployment":
59 | // Add annotation to pod spec:
60 | // kubectl.kubernetes.io/restartedAt: "2020-08-21T03:56:27Z"
61 | err = kubernetes.AnnotateTemplate(u, "clouddriver.spinnaker.io/restartedAt",
62 | time.Now().In(time.UTC).Format(time.RFC3339))
63 | if err != nil {
64 | clouddriver.Error(c, http.StatusInternalServerError, err)
65 | return
66 | }
67 |
68 | meta, err = provider.Client.Apply(u)
69 | if err != nil {
70 | clouddriver.Error(c, http.StatusInternalServerError, err)
71 | return
72 | }
73 |
74 | default:
75 | clouddriver.Error(c, http.StatusBadRequest, fmt.Errorf("restarting kind %s not currently supported", kind))
76 | return
77 | }
78 |
79 | kr := kubernetes.Resource{
80 | AccountName: rr.Account,
81 | ID: uuid.New().String(),
82 | TaskID: taskID,
83 | APIGroup: meta.Group,
84 | Name: meta.Name,
85 | Namespace: meta.Namespace,
86 | Resource: meta.Resource,
87 | Version: meta.Version,
88 | Kind: meta.Kind,
89 | SpinnakerApp: app,
90 | }
91 |
92 | err = cc.SQLClient.CreateKubernetesResource(kr)
93 | if err != nil {
94 | clouddriver.Error(c, http.StatusInternalServerError, err)
95 | return
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/internal/api/core/kubernetes/restart_test.go:
--------------------------------------------------------------------------------
1 | package kubernetes_test
2 |
3 | import (
4 | "errors"
5 | "net/http"
6 |
7 | "github.com/homedepot/go-clouddriver/internal/kubernetes"
8 | . "github.com/onsi/ginkgo/v2"
9 | . "github.com/onsi/gomega"
10 | )
11 |
12 | var _ = Describe("RollingRestart", func() {
13 | BeforeEach(func() {
14 | setup()
15 | })
16 |
17 | JustBeforeEach(func() {
18 | kubernetesController.RollingRestart(c, rollingRestartManifestRequest)
19 | })
20 |
21 | When("getting the provider returns an error", func() {
22 | BeforeEach(func() {
23 | fakeSQLClient.GetKubernetesProviderReturns(kubernetes.Provider{}, errors.New("error getting provider"))
24 | })
25 |
26 | It("returns an error", func() {
27 | Expect(c.Writer.Status()).To(Equal(http.StatusBadRequest))
28 | Expect(c.Errors.Last().Error()).To(Equal("internal: error getting kubernetes provider spin-cluster-account: error getting provider"))
29 | })
30 | })
31 |
32 | When("getting the manifest returns an error", func() {
33 | BeforeEach(func() {
34 | fakeKubeClient.GetReturns(nil, errors.New("error getting manifest"))
35 | })
36 |
37 | It("returns an error", func() {
38 | Expect(c.Writer.Status()).To(Equal(http.StatusInternalServerError))
39 | Expect(c.Errors.Last().Error()).To(Equal("error getting manifest"))
40 | })
41 | })
42 |
43 | When("applying the manifest returns an error", func() {
44 | BeforeEach(func() {
45 | fakeKubeClient.ApplyReturns(kubernetes.Metadata{}, errors.New("error applying manifest"))
46 | })
47 |
48 | It("returns an error", func() {
49 | Expect(c.Writer.Status()).To(Equal(http.StatusInternalServerError))
50 | Expect(c.Errors.Last().Error()).To(Equal("error applying manifest"))
51 | })
52 | })
53 |
54 | When("creating the resource returns an error", func() {
55 | BeforeEach(func() {
56 | fakeSQLClient.CreateKubernetesResourceReturns(errors.New("error creating resource"))
57 | })
58 |
59 | It("returns an error", func() {
60 | Expect(c.Writer.Status()).To(Equal(http.StatusInternalServerError))
61 | Expect(c.Errors.Last().Error()).To(Equal("error creating resource"))
62 | })
63 | })
64 |
65 | When("the kind is not supported to restarted", func() {
66 | BeforeEach(func() {
67 | rollingRestartManifestRequest.ManifestName = "not-supported-kind test-name"
68 | })
69 |
70 | It("returns an error", func() {
71 | Expect(c.Writer.Status()).To(Equal(http.StatusBadRequest))
72 | Expect(c.Errors.Last().Error()).To(Equal("restarting kind not-supported-kind not currently supported"))
73 | })
74 | })
75 |
76 | When("it succeeds", func() {
77 | It("succeeds", func() {
78 | Expect(c.Writer.Status()).To(Equal(http.StatusOK))
79 | })
80 | })
81 |
82 | When("Using a namespace-scoped provider", func() {
83 | BeforeEach(func() {
84 | fakeSQLClient.GetKubernetesProviderReturns(namespaceScopedProvider, nil)
85 | })
86 |
87 | When("the kind is not supported", func() {
88 | BeforeEach(func() {
89 | rollingRestartManifestRequest.ManifestName = "namespace someNamespace"
90 | })
91 |
92 | It("returns an error", func() {
93 | Expect(c.Writer.Status()).To(Equal(http.StatusBadRequest))
94 | Expect(c.Errors.Last().Error()).To(Equal("namespace-scoped account not allowed to access cluster-scoped kind: 'namespace'"))
95 | })
96 | })
97 |
98 | When("the kind is supported", func() {
99 | It("succeeds", func() {
100 | Expect(c.Writer.Status()).To(Equal(http.StatusOK))
101 | _, _, namespace := fakeKubeClient.GetArgsForCall(0)
102 | Expect(namespace).To(Equal("provider-namespace"))
103 | })
104 | })
105 | })
106 |
107 | When("Using a multiple namespace-scoped provider", func() {
108 | BeforeEach(func() {
109 | fakeSQLClient.GetKubernetesProviderReturns(multipleNamespaceScopedProvider, nil)
110 | })
111 |
112 | When("the kind is not supported", func() {
113 | BeforeEach(func() {
114 | rollingRestartManifestRequest.ManifestName = "namespace someNamespace"
115 | })
116 |
117 | It("returns an error", func() {
118 | Expect(c.Writer.Status()).To(Equal(http.StatusBadRequest))
119 | Expect(c.Errors.Last().Error()).To(Equal("namespace-scoped account not allowed to access cluster-scoped kind: 'namespace'"))
120 | })
121 | })
122 |
123 | When("the kind is supported", func() {
124 | BeforeEach(func() {
125 | rollingRestartManifestRequest.Location = "provider-namespace"
126 | })
127 | It("succeeds", func() {
128 | Expect(c.Writer.Status()).To(Equal(http.StatusOK))
129 | _, _, namespace := fakeKubeClient.GetArgsForCall(0)
130 | Expect(namespace).To(Equal("provider-namespace"))
131 | })
132 | })
133 | })
134 | })
135 |
--------------------------------------------------------------------------------
/internal/api/core/kubernetes/runjob.go:
--------------------------------------------------------------------------------
1 | package kubernetes
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/gin-gonic/gin"
7 | "github.com/google/uuid"
8 | "github.com/homedepot/go-clouddriver/internal/kubernetes"
9 | kube "github.com/homedepot/go-clouddriver/internal/kubernetes"
10 | clouddriver "github.com/homedepot/go-clouddriver/pkg"
11 | "k8s.io/apimachinery/pkg/util/rand"
12 | )
13 |
14 | const randNameNumber = 5
15 |
16 | func (cc *Controller) RunJob(c *gin.Context, rj RunJobRequest) {
17 | taskID := clouddriver.TaskIDFromContext(c)
18 |
19 | provider, err := cc.KubernetesProvider(rj.Account)
20 | if err != nil {
21 | clouddriver.Error(c, http.StatusBadRequest, err)
22 | return
23 | }
24 |
25 | u, err := kube.ToUnstructured(rj.Manifest)
26 | if err != nil {
27 | clouddriver.Error(c, http.StatusInternalServerError, err)
28 | return
29 | }
30 |
31 | namespace := ""
32 |
33 | // Preserve backwards compatibility
34 | if len(provider.Namespaces) == 1 {
35 | namespace = provider.Namespaces[0]
36 | }
37 |
38 | kubernetes.SetNamespaceOnManifest(&u, namespace)
39 |
40 | err = provider.ValidateNamespaceAccess(u.GetNamespace()) // pass in the current manifest's namespace
41 | if err != nil {
42 | clouddriver.Error(c, http.StatusBadRequest, err)
43 | return
44 | }
45 |
46 | err = kube.AddSpinnakerAnnotations(&u, rj.Application)
47 | if err != nil {
48 | clouddriver.Error(c, http.StatusInternalServerError, err)
49 | return
50 | }
51 |
52 | err = kube.AddSpinnakerLabels(&u, rj.Application)
53 | if err != nil {
54 | clouddriver.Error(c, http.StatusInternalServerError, err)
55 | return
56 | }
57 |
58 | name := u.GetName()
59 | generateName := u.GetGenerateName()
60 |
61 | if name == "" && generateName != "" {
62 | u.SetName(generateName + rand.String(randNameNumber))
63 | }
64 |
65 | kubernetes.BindArtifacts(&u, append(rj.RequiredArtifacts, rj.OptionalArtifacts...), rj.Account)
66 |
67 | meta := kubernetes.Metadata{}
68 | if kubernetes.Replace(u) {
69 | meta, err = provider.Client.Replace(&u)
70 | } else {
71 | meta, err = provider.Client.Apply(&u)
72 | }
73 |
74 | if err != nil {
75 | clouddriver.Error(c, http.StatusInternalServerError, err)
76 | return
77 | }
78 |
79 | kr := kubernetes.Resource{
80 | AccountName: rj.Account,
81 | ID: uuid.New().String(),
82 | TaskID: taskID,
83 | APIGroup: meta.Group,
84 | Name: meta.Name,
85 | Namespace: meta.Namespace,
86 | Resource: meta.Resource,
87 | Version: meta.Version,
88 | Kind: "job",
89 | SpinnakerApp: rj.Application,
90 | }
91 |
92 | err = cc.SQLClient.CreateKubernetesResource(kr)
93 | if err != nil {
94 | clouddriver.Error(c, http.StatusInternalServerError, err)
95 | return
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/internal/api/core/kubernetes/scale.go:
--------------------------------------------------------------------------------
1 | package kubernetes
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "strconv"
7 | "strings"
8 |
9 | "github.com/gin-gonic/gin"
10 | "github.com/google/uuid"
11 | "github.com/homedepot/go-clouddriver/internal/kubernetes"
12 | clouddriver "github.com/homedepot/go-clouddriver/pkg"
13 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
14 | )
15 |
16 | func (cc *Controller) Scale(c *gin.Context, sm ScaleManifestRequest) {
17 | app := c.GetHeader("X-Spinnaker-Application")
18 | taskID := clouddriver.TaskIDFromContext(c)
19 | namespace := sm.Location
20 |
21 | provider, err := cc.KubernetesProvider(sm.Account)
22 | if err != nil {
23 | clouddriver.Error(c, http.StatusBadRequest, err)
24 | return
25 | }
26 |
27 | // Preserve backwards compatibility
28 | if len(provider.Namespaces) == 1 {
29 | namespace = provider.Namespaces[0]
30 | }
31 |
32 | a := strings.Split(sm.ManifestName, " ")
33 | kind := a[0]
34 | name := a[1]
35 |
36 | err = provider.ValidateKindStatus(kind)
37 | if err != nil {
38 | clouddriver.Error(c, http.StatusBadRequest, err)
39 | return
40 | }
41 |
42 | err = provider.ValidateNamespaceAccess(namespace)
43 | if err != nil {
44 | clouddriver.Error(c, http.StatusBadRequest, err)
45 | return
46 | }
47 |
48 | u, err := provider.Client.Get(kind, name, namespace)
49 | if err != nil {
50 | clouddriver.Error(c, http.StatusInternalServerError, err)
51 | return
52 | }
53 |
54 | var meta kubernetes.Metadata
55 |
56 | switch strings.ToLower(kind) {
57 | case "deployment", "replicaset", "statefulset":
58 | r, err := strconv.Atoi(sm.Replicas)
59 | if err != nil {
60 | clouddriver.Error(c, http.StatusBadRequest, err)
61 | return
62 | }
63 |
64 | err = unstructured.SetNestedField(u.Object, int64(r), "spec", "replicas")
65 | if err != nil {
66 | clouddriver.Error(c, http.StatusBadRequest, err)
67 | return
68 | }
69 |
70 | meta, err = provider.Client.Apply(u)
71 | if err != nil {
72 | clouddriver.Error(c, http.StatusInternalServerError, err)
73 | return
74 | }
75 | default:
76 | clouddriver.Error(c, http.StatusBadRequest,
77 | fmt.Errorf("scaling kind %s not currently supported", kind))
78 | return
79 | }
80 |
81 | kr := kubernetes.Resource{
82 | AccountName: sm.Account,
83 | ID: uuid.New().String(),
84 | TaskID: taskID,
85 | APIGroup: meta.Group,
86 | Name: meta.Name,
87 | Namespace: meta.Namespace,
88 | Resource: meta.Resource,
89 | Version: meta.Version,
90 | Kind: meta.Kind,
91 | SpinnakerApp: app,
92 | }
93 |
94 | err = cc.SQLClient.CreateKubernetesResource(kr)
95 | if err != nil {
96 | clouddriver.Error(c, http.StatusInternalServerError, err)
97 | return
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/internal/api/core/ok.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import "github.com/gin-gonic/gin"
4 |
5 | func OK(*gin.Context) {}
6 |
--------------------------------------------------------------------------------
/internal/api/core/ok_test.go:
--------------------------------------------------------------------------------
1 | package core_test
2 |
3 | import (
4 | // . "github.com/homedepot/go-clouddriver/internal/api/v0"
5 |
6 | "net/http"
7 |
8 | . "github.com/onsi/ginkgo/v2"
9 | . "github.com/onsi/gomega"
10 | )
11 |
12 | var _ = Describe("Ok", func() {
13 | BeforeEach(func() {
14 | setup()
15 | uri = svr.URL + "/health"
16 | createRequest(http.MethodGet)
17 | })
18 |
19 | AfterEach(func() {
20 | svr.Close()
21 | })
22 |
23 | JustBeforeEach(func() {
24 | doRequest()
25 | })
26 |
27 | It("returns OK", func() {
28 | Expect(res.StatusCode).To(Equal(http.StatusOK))
29 | })
30 | })
31 |
--------------------------------------------------------------------------------
/internal/api/core/ops.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/gin-gonic/gin"
7 | "github.com/gin-gonic/gin/binding"
8 | "github.com/homedepot/go-clouddriver/internal/api/core/kubernetes"
9 | clouddriver "github.com/homedepot/go-clouddriver/pkg"
10 | )
11 |
12 | // CreateKubernetesOperation is the main function that starts a kubernetes operation.
13 | //
14 | // Kubernetes operations are things like deploy/delete manifest or perform
15 | // a rolling restart. Spinnaker sends *all* of these types of events to the
16 | // same endpoint (/kubernetes/ops), so we have to unmarshal and check which
17 | // kind of operation we are performing.
18 | //
19 | // The actual actions have been moved to the kubernetes subfolder to make
20 | // this function a bit more readable.
21 | func (cc *Controller) CreateKubernetesOperation(c *gin.Context) {
22 | // All operations are bound to a task ID and stored in the database.
23 | ko := kubernetes.Operations{}
24 | taskID := clouddriver.TaskIDFromContext(c)
25 |
26 | if err := c.ShouldBindBodyWith(&ko, binding.JSON); err != nil {
27 | clouddriver.Error(c, http.StatusBadRequest, err)
28 | return
29 | }
30 |
31 | kc := kubernetes.Controller{
32 | Controller: cc.Controller,
33 | }
34 | // Loop through each request in the kubernetes operations and perform
35 | // each requested action.
36 | for _, req := range ko {
37 | if req.DeployManifest != nil {
38 | kc.Deploy(c, *req.DeployManifest)
39 | }
40 |
41 | if req.DeleteManifest != nil {
42 | kc.Delete(c, *req.DeleteManifest)
43 | }
44 |
45 | if req.DisableManifest != nil {
46 | kc.Disable(c, *req.DisableManifest)
47 | }
48 |
49 | if req.EnableManifest != nil {
50 | kc.Enable(c, *req.EnableManifest)
51 | }
52 |
53 | if req.ScaleManifest != nil {
54 | kc.Scale(c, *req.ScaleManifest)
55 | }
56 |
57 | if req.CleanupArtifacts != nil {
58 | kc.CleanupArtifacts(c, *req.CleanupArtifacts)
59 | }
60 |
61 | if req.RollingRestartManifest != nil {
62 | kc.RollingRestart(c, *req.RollingRestartManifest)
63 | }
64 |
65 | if req.RunJob != nil {
66 | kc.RunJob(c, *req.RunJob)
67 | }
68 |
69 | if req.UndoRolloutManifest != nil {
70 | kc.Rollback(c, *req.UndoRolloutManifest)
71 | }
72 |
73 | if req.PatchManifest != nil {
74 | kc.Patch(c, *req.PatchManifest)
75 | }
76 |
77 | if c.Errors != nil && len(c.Errors) > 0 {
78 | return
79 | }
80 | }
81 |
82 | or := kubernetes.OperationsResponse{
83 | ID: taskID,
84 | ResourceURI: "/task/" + taskID,
85 | }
86 | c.JSON(http.StatusOK, or)
87 | }
88 |
--------------------------------------------------------------------------------
/internal/api/core/security_groups.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/gin-gonic/gin"
7 | )
8 |
9 | // I'm not sure what this endpoint is supposed to do or what Spinnaker does with it,
10 | // but here is an example of the response. I found that returning an empty object "{}"
11 | // satisfies Deck.
12 | //
13 | // {
14 | // "label": "securityGroups",
15 | // "name": "applications",
16 | // "profiles": [
17 | // "smoketests"
18 | // ],
19 | // "propertySources": [
20 | // {
21 | // "name": "vault:spinnaker",
22 | // "source": {
23 | // "kubernetes.accounts[0].cacheIntervalSeconds": 3600,
24 | // "kubernetes.accounts[0].cacheThreads": 4,
25 | // "kubernetes.accounts[0].cachingPolicies": "",
26 | // "kubernetes.accounts[0].checkPermissionsOnStartup": false,
27 | // "kubernetes.accounts[0].configureImagePullSecrets": true,
28 | // "kubernetes.accounts[0].context": "...",
29 | // "kubernetes.accounts[0].dockerRegistries[0].accountName": "docker-registry",
30 | // "kubernetes.accounts[0].dockerRegistries[0].namespaces": "",
31 | // "kubernetes.accounts[0].kubeconfigContents": "...",
32 | // "kubernetes.accounts[0].liveManifestCalls": true,
33 | // "kubernetes.accounts[0].metrics": false,
34 | // "kubernetes.accounts[0].name": "...",
35 | // "kubernetes.accounts[0].namespaces": "",
36 | // "kubernetes.accounts[0].oAuthScopes": "",
37 | // "kubernetes.accounts[0].omitKinds[0]": "podPreset",
38 | // "kubernetes.accounts[0].omitNamespaces[0]": "kube-public",
39 | // "kubernetes.accounts[0].omitNamespaces[1]": "kube-node-lease",
40 | // "kubernetes.accounts[0].onlySpinnakerManaged": true,
41 | // "kubernetes.accounts[0].permissions.READ[0]": "...",
42 | // "kubernetes.accounts[0].permissions.READ[1]": "...",
43 | // "kubernetes.accounts[0].permissions.WRITE[0]": "...",
44 | // "kubernetes.accounts[0].providerVersion": "V2",
45 | // "kubernetes.accounts[0].requiredGroupMembership": "",
46 | // }
47 | // }
48 | // ]
49 | // }
50 | func ListSecurityGroups(c *gin.Context) {
51 | var empty struct{}
52 |
53 | c.JSON(http.StatusOK, empty)
54 | }
55 |
--------------------------------------------------------------------------------
/internal/api/core/task.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "strings"
7 |
8 | clouddriver "github.com/homedepot/go-clouddriver/pkg"
9 |
10 | "github.com/gin-gonic/gin"
11 | "github.com/homedepot/go-clouddriver/internal/artifact"
12 | "github.com/homedepot/go-clouddriver/internal/kubernetes"
13 | "github.com/iancoleman/strcase"
14 | )
15 |
16 | // GetTask gets a task - currently only associated with kubernetes 'tasks'.
17 | func (cc *Controller) GetTask(c *gin.Context) {
18 | id := c.Param("id")
19 | task := clouddriver.NewDefaultTask(id)
20 | manifests := []map[string]interface{}{}
21 |
22 | resources, err := cc.SQLClient.ListKubernetesResourcesByTaskID(id)
23 | if err != nil {
24 | task.Status.Failed = true
25 | task.Status.Retryable = true
26 | task.Status.Status = fmt.Sprintf("Error listing resources for task (id: %s): %v", id, err)
27 | c.JSON(http.StatusInternalServerError, task)
28 |
29 | return
30 | }
31 |
32 | if len(resources) == 0 {
33 | task.Status.Failed = true
34 | task.Status.Status = fmt.Sprintf("Task not found (id: %s)", id)
35 | c.JSON(http.StatusNotFound, task)
36 |
37 | return
38 | }
39 |
40 | accountName := resources[0].AccountName
41 |
42 | provider, err := cc.KubernetesProvider(accountName)
43 | if err != nil {
44 | task.Status.Failed = true
45 | task.Status.Retryable = true
46 | task.Status.Status = fmt.Sprintf("Error getting kubernetes provider %s for task (id: %s): %v",
47 | accountName, id, err)
48 | c.JSON(http.StatusInternalServerError, task)
49 |
50 | return
51 | }
52 |
53 | for _, r := range resources {
54 | // Ignore getting the manifest if task type is "cleanup" or "noop".
55 | if strings.EqualFold(r.TaskType, clouddriver.TaskTypeCleanup) ||
56 | strings.EqualFold(r.TaskType, clouddriver.TaskTypeNoOp) {
57 | manifests = append(manifests, map[string]interface{}{})
58 |
59 | continue
60 | }
61 |
62 | result, err := provider.Client.Get(r.Resource, r.Name, r.Namespace)
63 | if err != nil {
64 | // If the task type is "delete" and the resource was not found,
65 | // append an empty manifest and continue.
66 | if strings.EqualFold(r.TaskType, clouddriver.TaskTypeDelete) &&
67 | strings.HasSuffix(err.Error(), "not found") {
68 | manifests = append(manifests, map[string]interface{}{})
69 |
70 | continue
71 | }
72 |
73 | task.Status.Failed = true
74 | task.Status.Retryable = true
75 | task.Status.Status = fmt.Sprintf("Error getting resource for task (task ID: %s, kind: %s, name: %s, namespace: %s): %v",
76 | id, r.Resource, r.Name, r.Namespace, err)
77 | c.JSON(http.StatusInternalServerError, task)
78 |
79 | return
80 | } else if strings.EqualFold(r.TaskType, clouddriver.TaskTypeDelete) {
81 | task.Status.Complete = false
82 | task.Status.Completed = false
83 | task.Status.Status = "Orchestration in progress."
84 | }
85 |
86 | manifests = append(manifests, result.Object)
87 | }
88 |
89 | mnr := buildMapOfNamespaceToResource(resources)
90 | // Refactor bound artifact to get the list of bound artifacts as not all created artifacts need to be bound.
91 | createdArtifacts := buildCreatedArtifacts(resources)
92 | ro := clouddriver.TaskResultObject{
93 | BoundArtifacts: createdArtifacts,
94 | DeployedNamesByLocation: mnr,
95 | CreatedArtifacts: createdArtifacts,
96 | Manifests: manifests,
97 | ManifestNamesByNamespace: mnr,
98 | ManifestNamesByNamespaceToRefresh: mnr,
99 | }
100 |
101 | task.ResultObjects = []clouddriver.TaskResultObject{ro}
102 |
103 | c.JSON(http.StatusOK, task)
104 | }
105 |
106 | func buildCreatedArtifacts(resources []kubernetes.Resource) []clouddriver.Artifact {
107 | var (
108 | artifactVersion string
109 | lastIndex int
110 | )
111 |
112 | cas := []clouddriver.Artifact{}
113 |
114 | for _, resource := range resources {
115 | artifactVersion = ""
116 | lastIndex = strings.LastIndex(resource.Name, "-v")
117 |
118 | if lastIndex != -1 {
119 | artifactVersion = resource.Name[lastIndex+1:]
120 | }
121 |
122 | ca := clouddriver.Artifact{
123 | CustomKind: false,
124 | Location: resource.Namespace,
125 | Metadata: clouddriver.ArtifactMetadata{
126 | Account: resource.AccountName,
127 | },
128 | Name: resource.ArtifactName,
129 | Reference: resource.Name,
130 | Type: artifact.Type("kubernetes/" + strcase.ToLowerCamel(resource.Kind)),
131 | Version: artifactVersion,
132 | }
133 | cas = append(cas, ca)
134 | }
135 |
136 | return cas
137 | }
138 |
139 | func buildMapOfNamespaceToResource(resources []kubernetes.Resource) map[string][]string {
140 | m := map[string][]string{}
141 |
142 | for _, resource := range resources {
143 | if _, ok := m[resource.Namespace]; !ok {
144 | m[resource.Namespace] = []string{}
145 | }
146 |
147 | a := m[resource.Namespace]
148 | a = append(a, fmt.Sprintf("%s %s", resource.Kind, resource.Name))
149 | m[resource.Namespace] = a
150 | }
151 |
152 | return m
153 | }
154 |
--------------------------------------------------------------------------------
/internal/api/core/test/expected-git-repo-master-subpath.tar.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/homedepot/go-clouddriver/d1c33abe8387abbdd3b4a953ca96dda6f91ee61f/internal/api/core/test/expected-git-repo-master-subpath.tar.gz
--------------------------------------------------------------------------------
/internal/api/core/test/expected-git-repo-master.tar.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/homedepot/go-clouddriver/d1c33abe8387abbdd3b4a953ca96dda6f91ee61f/internal/api/core/test/expected-git-repo-master.tar.gz
--------------------------------------------------------------------------------
/internal/api/core/test/expected-git-repo-test.tar.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/homedepot/go-clouddriver/d1c33abe8387abbdd3b4a953ca96dda6f91ee61f/internal/api/core/test/expected-git-repo-test.tar.gz
--------------------------------------------------------------------------------
/internal/api/core/test/git-repo-master.tar.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/homedepot/go-clouddriver/d1c33abe8387abbdd3b4a953ca96dda6f91ee61f/internal/api/core/test/git-repo-master.tar.gz
--------------------------------------------------------------------------------
/internal/api/core/test/git-repo-test.tar.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/homedepot/go-clouddriver/d1c33abe8387abbdd3b4a953ca96dda6f91ee61f/internal/api/core/test/git-repo-test.tar.gz
--------------------------------------------------------------------------------
/internal/api/v1/controller.go:
--------------------------------------------------------------------------------
1 | package v1
2 |
3 | import "github.com/homedepot/go-clouddriver/internal"
4 |
5 | // Controller holds all non request-scoped objects.
6 | type Controller struct {
7 | *internal.Controller
8 | }
9 |
--------------------------------------------------------------------------------
/internal/api/v1/v1_suite_test.go:
--------------------------------------------------------------------------------
1 | package v1_test
2 |
3 | import (
4 | . "github.com/onsi/ginkgo/v2"
5 | . "github.com/onsi/gomega"
6 |
7 | "testing"
8 | )
9 |
10 | func TestV1(t *testing.T) {
11 | RegisterFailHandler(Fail)
12 | RunSpecs(t, "V1 Suite")
13 | }
14 |
--------------------------------------------------------------------------------
/internal/artifact/artifact_suite_test.go:
--------------------------------------------------------------------------------
1 | package artifact_test
2 |
3 | import (
4 | "testing"
5 |
6 | . "github.com/onsi/ginkgo/v2"
7 | . "github.com/onsi/gomega"
8 | )
9 |
10 | func TestArtifact(t *testing.T) {
11 | RegisterFailHandler(Fail)
12 | RunSpecs(t, "Artifact Suite")
13 | }
14 |
--------------------------------------------------------------------------------
/internal/artifact/test/credentials/test-keyfile.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "service_account",
3 | "project_id": "test",
4 | "private_key_id": "1234567890",
5 | "private_key": "",
6 | "client_email": "test@test.iam.gserviceaccount.com",
7 | "client_id": "1234567890",
8 | "auth_uri": "https://accounts.google.com/o/oauth2/auth",
9 | "token_uri": "https://oauth2.googleapis.com/token",
10 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
11 | "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/cd-jenkins%40np-platforms-cd-thd.iam.gserviceaccount.com"
12 | }
--------------------------------------------------------------------------------
/internal/artifact/test/custom-object.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "custom-artifact",
3 | "types": [
4 | "custom/object"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/internal/artifact/test/docker-image.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "docker-registry",
3 | "types": [
4 | "docker/image"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/internal/artifact/test/embedded-base64.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "embedded-artifact",
3 | "types": [
4 | "embedded/base64"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/internal/artifact/test/front50-pipeline-template.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "front50ArtifactCredentials",
3 | "types": [
4 | "front50/pipelineTemplate"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/internal/artifact/test/gcs-object-credentials.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gcs-test",
3 | "types": [
4 | "gcs/object"
5 | ],
6 | "jsonPath": "test/credentials/test-keyfile.json"
7 | }
8 |
--------------------------------------------------------------------------------
/internal/artifact/test/gcs-object.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gcs-spinnaker",
3 | "types": [
4 | "gcs/object"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/internal/artifact/test/git-repo-token.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ghe-spinnaker",
3 | "token": "fake-token",
4 | "types": [
5 | "git/repo"
6 | ]
7 | }
--------------------------------------------------------------------------------
/internal/artifact/test/git-repo.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "github-spinnaker",
3 | "types": [
4 | "git/repo"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/internal/artifact/test/github-enterprise-file.json:
--------------------------------------------------------------------------------
1 | {
2 | "baseURL": "https://github.example.com",
3 | "enterprise": true,
4 | "token": "some-token",
5 | "name": "github.example.com",
6 | "types": [
7 | "github/file"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/internal/artifact/test/github-file.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "github.com",
3 | "types": [
4 | "github/file"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/internal/artifact/test/helm-chart-basic-auth.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "helm-basic-auth",
3 | "types": [
4 | "helm/chart"
5 | ],
6 | "repository": "https://kubernetes-charts.storage.googleapis.com",
7 | "username": "fake-user",
8 | "password": "fakk-password"
9 | }
10 |
--------------------------------------------------------------------------------
/internal/artifact/test/helm-chart.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "helm-test",
3 | "types": [
4 | "helm/chart"
5 | ],
6 | "repository": "https://kubernetes-charts.storage.googleapis.com"
7 | }
8 |
--------------------------------------------------------------------------------
/internal/artifact/test/http-file.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "http",
3 | "types": [
4 | "http/file"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/internal/artifact/test/kubernetes.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "kubernetes",
3 | "types": [
4 | "kubernetes/configMap",
5 | "kubernetes/deployment",
6 | "kubernetes/replicaSet",
7 | "kubernetes/secret"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/internal/fiat/client.go:
--------------------------------------------------------------------------------
1 | package fiat
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io"
7 | "net/http"
8 | )
9 |
10 | const (
11 | defaultFiatURL = "http://spin-fiat.spinnaker:7003"
12 | )
13 |
14 | //go:generate counterfeiter . Client
15 | type Client interface {
16 | Authorize(account string) (Response, error)
17 | }
18 |
19 | func NewClient(url string) Client {
20 | return &client{
21 | url: url,
22 | }
23 | }
24 |
25 | func NewDefaultClient() Client {
26 | return NewClient(defaultFiatURL)
27 | }
28 |
29 | type client struct {
30 | url string
31 | }
32 |
33 | type Response struct {
34 | Name string `json:"name"`
35 | Accounts []Account `json:"accounts"`
36 | Applications []Application `json:"applications"`
37 | ServiceAccounts []ServiceAccount `json:"serviceAccounts"`
38 | Roles []Role `json:"roles"`
39 | BuildServices []interface{} `json:"buildServices"`
40 | ExtensionResources struct {
41 | } `json:"extensionResources"`
42 | Admin bool `json:"admin"`
43 | LegacyFallback bool `json:"legacyFallback"`
44 | AllowAccessToUnknownApplications bool `json:"allowAccessToUnknownApplications"`
45 | }
46 |
47 | type Account struct {
48 | Name string `json:"name"`
49 | Authorizations []string `json:"authorizations"`
50 | }
51 |
52 | type Application struct {
53 | Name string `json:"name"`
54 | Authorizations []string `json:"authorizations"`
55 | }
56 |
57 | type ServiceAccount struct {
58 | Name string `json:"name"`
59 | MemberOf []string `json:"memberOf"`
60 | }
61 |
62 | type Role struct {
63 | Name string `json:"name"`
64 | Source string `json:"source"`
65 | }
66 |
67 | func (c *client) Authorize(account string) (Response, error) {
68 | req, err := http.NewRequest(http.MethodGet, c.url+"/authorize/"+account, nil)
69 | if err != nil {
70 | return Response{}, err
71 | }
72 |
73 | res, err := http.DefaultClient.Do(req)
74 | if err != nil {
75 | return Response{}, err
76 | }
77 | defer res.Body.Close()
78 |
79 | if res.StatusCode < 200 || res.StatusCode > 399 {
80 | return Response{}, fmt.Errorf("user authorization error: %s", res.Status)
81 | }
82 |
83 | b, err := io.ReadAll(res.Body)
84 | if err != nil {
85 | return Response{}, err
86 | }
87 |
88 | response := Response{}
89 |
90 | err = json.Unmarshal(b, &response)
91 | if err != nil {
92 | return response, err
93 | }
94 |
95 | return response, nil
96 | }
97 |
--------------------------------------------------------------------------------
/internal/fiat/client_test.go:
--------------------------------------------------------------------------------
1 | package fiat_test
2 |
3 | import (
4 | "encoding/json"
5 | "net/http"
6 |
7 | . "github.com/homedepot/go-clouddriver/internal/fiat"
8 | . "github.com/onsi/ginkgo/v2"
9 | . "github.com/onsi/gomega"
10 | "github.com/onsi/gomega/ghttp"
11 | )
12 |
13 | var _ = Describe("Client", func() {
14 | var (
15 | server *ghttp.Server
16 | client Client
17 | err error
18 | response, expectedResponse Response
19 | fakeResponse string
20 | )
21 |
22 | BeforeEach(func() {
23 | server = ghttp.NewServer()
24 | client = NewClient(server.URL())
25 | })
26 |
27 | AfterEach(func() {
28 | server.Close()
29 | })
30 |
31 | Describe("#NewDefaultClient", func() {
32 | BeforeEach(func() {
33 | client = NewDefaultClient()
34 | })
35 |
36 | It("succeeds", func() {
37 | })
38 | })
39 |
40 | Describe("#Authorize", func() {
41 | JustBeforeEach(func() {
42 | response, err = client.Authorize("fakeAccount")
43 | })
44 |
45 | When("the uri is invalid", func() {
46 | BeforeEach(func() {
47 | client = NewClient("::haha")
48 | })
49 |
50 | It("returns an error", func() {
51 | Expect(err).ToNot(BeNil())
52 | Expect(response).To(Equal(Response{}))
53 | })
54 | })
55 |
56 | When("the server is not reachable", func() {
57 | BeforeEach(func() {
58 | server.Close()
59 | })
60 |
61 | It("returns an error", func() {
62 | Expect(err).ToNot(BeNil())
63 | })
64 | })
65 |
66 | When("the response is not 2XX", func() {
67 | BeforeEach(func() {
68 | server.AppendHandlers(
69 | ghttp.RespondWith(http.StatusInternalServerError, nil),
70 | )
71 | })
72 |
73 | It("returns an error", func() {
74 | Expect(err).ToNot(BeNil())
75 | Expect(err.Error()).To(Equal("user authorization error: 500 Internal Server Error"))
76 | })
77 | })
78 |
79 | When("the server returns bad data", func() {
80 | BeforeEach(func() {
81 | server.AppendHandlers(
82 | ghttp.RespondWith(http.StatusOK, ";{["),
83 | )
84 | })
85 |
86 | It("returns an error", func() {
87 | Expect(err).ToNot(BeNil())
88 | Expect(err.Error()).To(Equal("invalid character ';' looking for beginning of value"))
89 | })
90 | })
91 |
92 | When("it succeeds", func() {
93 | BeforeEach(func() {
94 | fakeResponse = `{
95 | "name" : "test_group",
96 | "accounts" : [ {
97 | "name" : "gke_github-replication-sandbox_us-central1_sandbox-us-central1-agent_smoketest-dev",
98 | "authorizations" : [ "READ", "WRITE", "EXECUTE", "CREATE" ]
99 | }, {
100 | "name" : "spin-cluster-account",
101 | "authorizations" : [ "READ", "WRITE", "EXECUTE", "CREATE" ]
102 | } ],
103 | "serviceAccounts" : [ {
104 | "name" : "gg_cloud_gcp_spinnaker_admins_member",
105 | "memberOf" : [ "gg_cloud_gcp_spinnaker_admins" ]
106 | } ],
107 | "roles" : [ {
108 | "name" : "gg_cloud_gcp_spinnaker_admins",
109 | "source" : "EXTERNAL"
110 | }, {
111 | "name" : "test_group",
112 | "source" : "EXTERNAL"
113 | } ],
114 | "buildServices" : [ ],
115 | "extensionResources" : { },
116 | "admin" : false,
117 | "legacyFallback" : false,
118 | "allowAccessToUnknownApplications" : false
119 | }
120 | `
121 |
122 | server.AppendHandlers(ghttp.CombineHandlers(
123 | ghttp.VerifyRequest(http.MethodGet, "/authorize/fakeAccount"),
124 | ghttp.RespondWith(http.StatusOK, fakeResponse),
125 | ))
126 | })
127 |
128 | It("succeeds", func() {
129 | byt := []byte(fakeResponse)
130 | err := json.Unmarshal(byt, &expectedResponse)
131 | Expect(err).To(BeNil())
132 | Expect(expectedResponse).To(Equal(response))
133 | })
134 | })
135 | })
136 | })
137 |
--------------------------------------------------------------------------------
/internal/fiat/fiat_suite_test.go:
--------------------------------------------------------------------------------
1 | package fiat_test
2 |
3 | import (
4 | "testing"
5 |
6 | . "github.com/onsi/ginkgo/v2"
7 | . "github.com/onsi/gomega"
8 | )
9 |
10 | func TestFiat(t *testing.T) {
11 | RegisterFailHandler(Fail)
12 | RunSpecs(t, "Fiat Suite")
13 | }
14 |
--------------------------------------------------------------------------------
/internal/fiat/fiatfakes/fake_client.go:
--------------------------------------------------------------------------------
1 | // Code generated by counterfeiter. DO NOT EDIT.
2 | package fiatfakes
3 |
4 | import (
5 | "sync"
6 |
7 | "github.com/homedepot/go-clouddriver/internal/fiat"
8 | )
9 |
10 | type FakeClient struct {
11 | AuthorizeStub func(string) (fiat.Response, error)
12 | authorizeMutex sync.RWMutex
13 | authorizeArgsForCall []struct {
14 | arg1 string
15 | }
16 | authorizeReturns struct {
17 | result1 fiat.Response
18 | result2 error
19 | }
20 | authorizeReturnsOnCall map[int]struct {
21 | result1 fiat.Response
22 | result2 error
23 | }
24 | invocations map[string][][]interface{}
25 | invocationsMutex sync.RWMutex
26 | }
27 |
28 | func (fake *FakeClient) Authorize(arg1 string) (fiat.Response, error) {
29 | fake.authorizeMutex.Lock()
30 | ret, specificReturn := fake.authorizeReturnsOnCall[len(fake.authorizeArgsForCall)]
31 | fake.authorizeArgsForCall = append(fake.authorizeArgsForCall, struct {
32 | arg1 string
33 | }{arg1})
34 | stub := fake.AuthorizeStub
35 | fakeReturns := fake.authorizeReturns
36 | fake.recordInvocation("Authorize", []interface{}{arg1})
37 | fake.authorizeMutex.Unlock()
38 | if stub != nil {
39 | return stub(arg1)
40 | }
41 | if specificReturn {
42 | return ret.result1, ret.result2
43 | }
44 | return fakeReturns.result1, fakeReturns.result2
45 | }
46 |
47 | func (fake *FakeClient) AuthorizeCallCount() int {
48 | fake.authorizeMutex.RLock()
49 | defer fake.authorizeMutex.RUnlock()
50 | return len(fake.authorizeArgsForCall)
51 | }
52 |
53 | func (fake *FakeClient) AuthorizeCalls(stub func(string) (fiat.Response, error)) {
54 | fake.authorizeMutex.Lock()
55 | defer fake.authorizeMutex.Unlock()
56 | fake.AuthorizeStub = stub
57 | }
58 |
59 | func (fake *FakeClient) AuthorizeArgsForCall(i int) string {
60 | fake.authorizeMutex.RLock()
61 | defer fake.authorizeMutex.RUnlock()
62 | argsForCall := fake.authorizeArgsForCall[i]
63 | return argsForCall.arg1
64 | }
65 |
66 | func (fake *FakeClient) AuthorizeReturns(result1 fiat.Response, result2 error) {
67 | fake.authorizeMutex.Lock()
68 | defer fake.authorizeMutex.Unlock()
69 | fake.AuthorizeStub = nil
70 | fake.authorizeReturns = struct {
71 | result1 fiat.Response
72 | result2 error
73 | }{result1, result2}
74 | }
75 |
76 | func (fake *FakeClient) AuthorizeReturnsOnCall(i int, result1 fiat.Response, result2 error) {
77 | fake.authorizeMutex.Lock()
78 | defer fake.authorizeMutex.Unlock()
79 | fake.AuthorizeStub = nil
80 | if fake.authorizeReturnsOnCall == nil {
81 | fake.authorizeReturnsOnCall = make(map[int]struct {
82 | result1 fiat.Response
83 | result2 error
84 | })
85 | }
86 | fake.authorizeReturnsOnCall[i] = struct {
87 | result1 fiat.Response
88 | result2 error
89 | }{result1, result2}
90 | }
91 |
92 | func (fake *FakeClient) Invocations() map[string][][]interface{} {
93 | fake.invocationsMutex.RLock()
94 | defer fake.invocationsMutex.RUnlock()
95 | fake.authorizeMutex.RLock()
96 | defer fake.authorizeMutex.RUnlock()
97 | copiedInvocations := map[string][][]interface{}{}
98 | for key, value := range fake.invocations {
99 | copiedInvocations[key] = value
100 | }
101 | return copiedInvocations
102 | }
103 |
104 | func (fake *FakeClient) recordInvocation(key string, args []interface{}) {
105 | fake.invocationsMutex.Lock()
106 | defer fake.invocationsMutex.Unlock()
107 | if fake.invocations == nil {
108 | fake.invocations = map[string][][]interface{}{}
109 | }
110 | if fake.invocations[key] == nil {
111 | fake.invocations[key] = [][]interface{}{}
112 | }
113 | fake.invocations[key] = append(fake.invocations[key], args)
114 | }
115 |
116 | var _ fiat.Client = new(FakeClient)
117 |
--------------------------------------------------------------------------------
/internal/front50/client.go:
--------------------------------------------------------------------------------
1 | package front50
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io"
7 | "net/http"
8 | )
9 |
10 | const (
11 | defaultFront50Url = "http://spin-front50.spinnaker:8080"
12 | )
13 |
14 | //go:generate counterfeiter . Client
15 | type Client interface {
16 | Project(project string) (Response, error)
17 | }
18 |
19 | func NewClient(url string) Client {
20 | return &client{
21 | url: url,
22 | }
23 | }
24 |
25 | func NewDefaultClient() Client {
26 | return NewClient(defaultFront50Url)
27 | }
28 |
29 | type client struct {
30 | url string
31 | }
32 |
33 | type Response struct {
34 | ID string `json:"id"`
35 | Name string `json:"name"`
36 | Email string `json:"email"`
37 | Config Config `json:"config"`
38 | UpdateTs int64 `json:"updateTs"`
39 | CreateTs int64 `json:"createTs"`
40 | LastModifiedBy string `json:"lastModifiedBy"`
41 | }
42 |
43 | type Config struct {
44 | PipelineConfigs []PipelineConfig `json:"pipelineConfigs"`
45 | Applications []string `json:"applications"`
46 | Clusters []Cluster `json:"clusters"`
47 | }
48 |
49 | type PipelineConfig struct {
50 | Application string `json:"application"`
51 | PipelineConfigID string `json:"pipelineConfigId"`
52 | }
53 |
54 | type Cluster struct {
55 | Account string `json:"account"`
56 | Stack string `json:"stack"`
57 | Detail string `json:"detail"`
58 | Applications []string `json:"applications"`
59 | }
60 |
61 | // Project gets the Spinnaker project from the front50 service.
62 | //
63 | // See https://github.com/spinnaker/front50/blob/master/front50-web/src/main/java/com/netflix/spinnaker/front50/controllers/v2/ProjectsController.java
64 | func (c *client) Project(project string) (Response, error) {
65 | req, err := http.NewRequest(http.MethodGet, c.url+"/v2/projects/"+project, nil)
66 | if err != nil {
67 | return Response{}, err
68 | }
69 |
70 | res, err := http.DefaultClient.Do(req)
71 | if err != nil {
72 | return Response{}, err
73 | }
74 | defer res.Body.Close()
75 |
76 | if res.StatusCode < 200 || res.StatusCode > 399 {
77 | return Response{}, fmt.Errorf("user authorization error: %s", res.Status)
78 | }
79 |
80 | b, err := io.ReadAll(res.Body)
81 | if err != nil {
82 | return Response{}, err
83 | }
84 |
85 | response := Response{}
86 |
87 | err = json.Unmarshal(b, &response)
88 | if err != nil {
89 | return response, err
90 | }
91 |
92 | return response, nil
93 | }
94 |
--------------------------------------------------------------------------------
/internal/front50/client_test.go:
--------------------------------------------------------------------------------
1 | package front50_test
2 |
3 | import (
4 | "encoding/json"
5 | "net/http"
6 |
7 | . "github.com/homedepot/go-clouddriver/internal/front50"
8 | . "github.com/onsi/ginkgo/v2"
9 | . "github.com/onsi/gomega"
10 | "github.com/onsi/gomega/ghttp"
11 | )
12 |
13 | var _ = Describe("Client", func() {
14 | var (
15 | server *ghttp.Server
16 | client Client
17 | err error
18 | response, expectedResponse Response
19 | fakeResponse string
20 | )
21 |
22 | BeforeEach(func() {
23 | server = ghttp.NewServer()
24 | client = NewClient(server.URL())
25 | })
26 |
27 | AfterEach(func() {
28 | server.Close()
29 | })
30 |
31 | Describe("#NewDefaultClient", func() {
32 | BeforeEach(func() {
33 | client = NewDefaultClient()
34 | })
35 |
36 | It("succeeds", func() {
37 | })
38 | })
39 |
40 | Describe("#Project", func() {
41 | JustBeforeEach(func() {
42 | response, err = client.Project("fakeProjectID")
43 | })
44 |
45 | When("the uri is invalid", func() {
46 | BeforeEach(func() {
47 | client = NewClient("::haha")
48 | })
49 |
50 | It("returns an error", func() {
51 | Expect(err).ToNot(BeNil())
52 | Expect(response).To(Equal(Response{}))
53 | })
54 | })
55 |
56 | When("the server is not reachable", func() {
57 | BeforeEach(func() {
58 | server.Close()
59 | })
60 |
61 | It("returns an error", func() {
62 | Expect(err).ToNot(BeNil())
63 | })
64 | })
65 |
66 | When("the response is not 2XX", func() {
67 | BeforeEach(func() {
68 | server.AppendHandlers(
69 | ghttp.RespondWith(http.StatusInternalServerError, nil),
70 | )
71 | })
72 |
73 | It("returns an error", func() {
74 | Expect(err).ToNot(BeNil())
75 | Expect(err.Error()).To(Equal("user authorization error: 500 Internal Server Error"))
76 | })
77 | })
78 |
79 | When("the server returns bad data", func() {
80 | BeforeEach(func() {
81 | server.AppendHandlers(
82 | ghttp.RespondWith(http.StatusOK, ";{["),
83 | )
84 | })
85 |
86 | It("returns an error", func() {
87 | Expect(err).ToNot(BeNil())
88 | Expect(err.Error()).To(Equal("invalid character ';' looking for beginning of value"))
89 | })
90 | })
91 |
92 | When("it succeeds", func() {
93 | BeforeEach(func() {
94 | fakeResponse = `{
95 | "id": "048de9a7-7b57-4097-8444-e44682d9dcfc",
96 | "name": "spinnaker",
97 | "email": "david_m_rogers@homedepot.com",
98 | "config": {
99 | "pipelineConfigs": [
100 | {
101 | "application": "billy",
102 | "pipelineConfigId": "b1bb2476-388b-47b9-8730-9617cccfe458"
103 | }
104 | ],
105 | "applications": [
106 | "smoketests"
107 | ],
108 | "clusters": [
109 | {
110 | "account": "gae_np-te-cd-tools-np",
111 | "stack": "*",
112 | "detail": "*",
113 | "applications": null
114 | },
115 | {
116 | "account": "gke_np-te-cd-tools_us-central1_smoketests_sandbox-us-central1-np",
117 | "stack": "*",
118 | "detail": "*",
119 | "applications": null
120 | },
121 | {
122 | "account": "gke_np-te-cd-tools_us-central1_sandbox-us-central1-np",
123 | "stack": "*",
124 | "detail": "*",
125 | "applications": null
126 | }
127 | ]
128 | },
129 | "updateTs": 1635962823717,
130 | "createTs": 1587655303067,
131 | "lastModifiedBy": "DXR05"
132 | }`
133 |
134 | server.AppendHandlers(ghttp.CombineHandlers(
135 | ghttp.VerifyRequest(http.MethodGet, "/v2/projects/fakeProjectID"),
136 | ghttp.RespondWith(http.StatusOK, fakeResponse),
137 | ))
138 | })
139 |
140 | It("succeeds", func() {
141 | byt := []byte(fakeResponse)
142 | err := json.Unmarshal(byt, &expectedResponse)
143 | Expect(err).To(BeNil())
144 | Expect(expectedResponse).To(Equal(response))
145 | })
146 | })
147 | })
148 | })
149 |
--------------------------------------------------------------------------------
/internal/front50/front50_suite_test.go:
--------------------------------------------------------------------------------
1 | package front50_test
2 |
3 | import (
4 | "testing"
5 |
6 | . "github.com/onsi/ginkgo/v2"
7 | . "github.com/onsi/gomega"
8 | )
9 |
10 | func TestFront50(t *testing.T) {
11 | RegisterFailHandler(Fail)
12 | RunSpecs(t, "Front50 Suite")
13 | }
14 |
--------------------------------------------------------------------------------
/internal/front50/front50fakes/fake_client.go:
--------------------------------------------------------------------------------
1 | // Code generated by counterfeiter. DO NOT EDIT.
2 | package front50fakes
3 |
4 | import (
5 | "sync"
6 |
7 | "github.com/homedepot/go-clouddriver/internal/front50"
8 | )
9 |
10 | type FakeClient struct {
11 | ProjectStub func(string) (front50.Response, error)
12 | projectMutex sync.RWMutex
13 | projectArgsForCall []struct {
14 | arg1 string
15 | }
16 | projectReturns struct {
17 | result1 front50.Response
18 | result2 error
19 | }
20 | projectReturnsOnCall map[int]struct {
21 | result1 front50.Response
22 | result2 error
23 | }
24 | invocations map[string][][]interface{}
25 | invocationsMutex sync.RWMutex
26 | }
27 |
28 | func (fake *FakeClient) Project(arg1 string) (front50.Response, error) {
29 | fake.projectMutex.Lock()
30 | ret, specificReturn := fake.projectReturnsOnCall[len(fake.projectArgsForCall)]
31 | fake.projectArgsForCall = append(fake.projectArgsForCall, struct {
32 | arg1 string
33 | }{arg1})
34 | stub := fake.ProjectStub
35 | fakeReturns := fake.projectReturns
36 | fake.recordInvocation("Project", []interface{}{arg1})
37 | fake.projectMutex.Unlock()
38 | if stub != nil {
39 | return stub(arg1)
40 | }
41 | if specificReturn {
42 | return ret.result1, ret.result2
43 | }
44 | return fakeReturns.result1, fakeReturns.result2
45 | }
46 |
47 | func (fake *FakeClient) ProjectCallCount() int {
48 | fake.projectMutex.RLock()
49 | defer fake.projectMutex.RUnlock()
50 | return len(fake.projectArgsForCall)
51 | }
52 |
53 | func (fake *FakeClient) ProjectCalls(stub func(string) (front50.Response, error)) {
54 | fake.projectMutex.Lock()
55 | defer fake.projectMutex.Unlock()
56 | fake.ProjectStub = stub
57 | }
58 |
59 | func (fake *FakeClient) ProjectArgsForCall(i int) string {
60 | fake.projectMutex.RLock()
61 | defer fake.projectMutex.RUnlock()
62 | argsForCall := fake.projectArgsForCall[i]
63 | return argsForCall.arg1
64 | }
65 |
66 | func (fake *FakeClient) ProjectReturns(result1 front50.Response, result2 error) {
67 | fake.projectMutex.Lock()
68 | defer fake.projectMutex.Unlock()
69 | fake.ProjectStub = nil
70 | fake.projectReturns = struct {
71 | result1 front50.Response
72 | result2 error
73 | }{result1, result2}
74 | }
75 |
76 | func (fake *FakeClient) ProjectReturnsOnCall(i int, result1 front50.Response, result2 error) {
77 | fake.projectMutex.Lock()
78 | defer fake.projectMutex.Unlock()
79 | fake.ProjectStub = nil
80 | if fake.projectReturnsOnCall == nil {
81 | fake.projectReturnsOnCall = make(map[int]struct {
82 | result1 front50.Response
83 | result2 error
84 | })
85 | }
86 | fake.projectReturnsOnCall[i] = struct {
87 | result1 front50.Response
88 | result2 error
89 | }{result1, result2}
90 | }
91 |
92 | func (fake *FakeClient) Invocations() map[string][][]interface{} {
93 | fake.invocationsMutex.RLock()
94 | defer fake.invocationsMutex.RUnlock()
95 | fake.projectMutex.RLock()
96 | defer fake.projectMutex.RUnlock()
97 | copiedInvocations := map[string][][]interface{}{}
98 | for key, value := range fake.invocations {
99 | copiedInvocations[key] = value
100 | }
101 | return copiedInvocations
102 | }
103 |
104 | func (fake *FakeClient) recordInvocation(key string, args []interface{}) {
105 | fake.invocationsMutex.Lock()
106 | defer fake.invocationsMutex.Unlock()
107 | if fake.invocations == nil {
108 | fake.invocations = map[string][][]interface{}{}
109 | }
110 | if fake.invocations[key] == nil {
111 | fake.invocations[key] = [][]interface{}{}
112 | }
113 | fake.invocations[key] = append(fake.invocations[key], args)
114 | }
115 |
116 | var _ front50.Client = new(FakeClient)
117 |
--------------------------------------------------------------------------------
/internal/helm/client.go:
--------------------------------------------------------------------------------
1 | package helm
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "io"
7 | "net/http"
8 | "sync"
9 |
10 | "gopkg.in/yaml.v2"
11 | )
12 |
13 | var (
14 | errUnableToFindResource = errors.New("unable to find resource")
15 | )
16 |
17 | type Index struct {
18 | APIVersion string `json:"apiVersion" yaml:"apiVersion"`
19 | Entries map[string][]Resource `json:"entries" yaml:"entries"`
20 | }
21 |
22 | type Resource struct {
23 | APIVersion string `json:"apiVersion" yaml:"apiVersion"`
24 | AppVersion string `json:"appVersion" yaml:"appVersion"`
25 | // Created time.Time `json:"created"`
26 | Description string `json:"description" yaml:"description"`
27 | Digest string `json:"digest" yaml:"digest"`
28 | Home string `json:"home" yaml:"home"`
29 | // Maintainers []struct {
30 | // Email string `json:"email"`
31 | // Name string `json:"name"`
32 | // } `json:"maintainers"`
33 | Name string `json:"name" yaml:"name"`
34 | Urls []string `json:"urls" yaml:"urls"`
35 | Version string `json:"version" yaml:"version"`
36 | }
37 |
38 | //go:generate counterfeiter . Client
39 | type Client interface {
40 | GetIndex() (Index, error)
41 | WithUsernameAndPassword(string, string)
42 | GetChart(string, string) ([]byte, error)
43 | }
44 |
45 | var (
46 | etag string
47 | cache Index
48 | mux sync.Mutex
49 | )
50 |
51 | func NewClient(url string) Client {
52 | return &client{url: url}
53 | }
54 |
55 | type client struct {
56 | url string
57 | username string
58 | password string
59 | }
60 |
61 | func (c *client) WithUsernameAndPassword(username, password string) {
62 | c.username = username
63 | c.password = password
64 | }
65 |
66 | func (c *client) GetIndex() (Index, error) {
67 | i := Index{}
68 |
69 | req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/index.yaml", c.url), nil)
70 | if err != nil {
71 | return i, err
72 | }
73 |
74 | req.Header.Add("If-None-Match", etag)
75 |
76 | if c.username != "" && c.password != "" {
77 | req.SetBasicAuth(c.username, c.password)
78 | }
79 |
80 | res, err := http.DefaultClient.Do(req)
81 | if err != nil {
82 | return i, err
83 | }
84 | defer res.Body.Close()
85 |
86 | if res.StatusCode == http.StatusNotModified {
87 | mux.Lock()
88 | defer mux.Unlock()
89 |
90 | return cache, nil
91 | }
92 |
93 | if res.StatusCode < 200 || res.StatusCode > 399 {
94 | return i, errors.New("error getting helm index: " + res.Status)
95 | }
96 |
97 | b, err := io.ReadAll(res.Body)
98 | if err != nil {
99 | return i, err
100 | }
101 |
102 | err = yaml.Unmarshal(b, &i)
103 | if err != nil {
104 | return i, err
105 | }
106 |
107 | mux.Lock()
108 | defer mux.Unlock()
109 |
110 | cache = i
111 | etag = res.Header.Get("etag")
112 |
113 | return i, nil
114 | }
115 |
116 | func (c *client) GetChart(name, version string) ([]byte, error) {
117 | var (
118 | err error
119 | b []byte
120 | )
121 |
122 | resource, err := c.findResource(name, version)
123 | if err != nil {
124 | return b, fmt.Errorf("helm: unable to find chart %s-%s: %w", name, version, err)
125 | }
126 |
127 | if len(resource.Urls) == 0 {
128 | return b, fmt.Errorf("helm: no resource urls defined for chart %s-%s", name, version)
129 | }
130 |
131 | // Loop through all the resource's URLs to get the chart.
132 | for _, url := range resource.Urls {
133 | req, e := http.NewRequest(http.MethodGet, url, nil)
134 | if e != nil {
135 | err = e
136 |
137 | continue
138 | }
139 | // Set credentials to pull the chart.
140 | if c.username != "" && c.password != "" {
141 | req.SetBasicAuth(c.username, c.password)
142 | }
143 |
144 | res, e := http.DefaultClient.Do(req)
145 | if e != nil {
146 | err = e
147 |
148 | continue
149 | }
150 | defer res.Body.Close()
151 |
152 | if res.StatusCode < 200 || res.StatusCode > 399 {
153 | err = errors.New("helm: error getting chart: " + res.Status)
154 |
155 | continue
156 | }
157 |
158 | b, err = io.ReadAll(res.Body)
159 | if err != nil {
160 | continue
161 | }
162 |
163 | break
164 | }
165 |
166 | return b, err
167 | }
168 |
169 | // findResource resets the helm index's cache then gets the resource
170 | // from the cache by name and version.
171 | //
172 | // If it is unable to find the resource it returns an error.
173 | func (c *client) findResource(name, version string) (Resource, error) {
174 | // Refresh the cached index.
175 | _, err := c.GetIndex()
176 | if err != nil {
177 | return Resource{}, err
178 | }
179 |
180 | // Lock since we are accessing the cached index.
181 | mux.Lock()
182 | defer mux.Unlock()
183 |
184 | if _, ok := cache.Entries[name]; ok {
185 | resources := cache.Entries[name]
186 | for _, resource := range resources {
187 | if resource.Version == version {
188 | return resource, nil
189 | }
190 | }
191 | }
192 |
193 | return Resource{}, errUnableToFindResource
194 | }
195 |
--------------------------------------------------------------------------------
/internal/helm/helm_suite_test.go:
--------------------------------------------------------------------------------
1 | package helm_test
2 |
3 | import (
4 | "testing"
5 |
6 | . "github.com/onsi/ginkgo/v2"
7 | . "github.com/onsi/gomega"
8 | )
9 |
10 | func TestHelm(t *testing.T) {
11 | RegisterFailHandler(Fail)
12 | RunSpecs(t, "Helm Suite")
13 | }
14 |
--------------------------------------------------------------------------------
/internal/internal_suite_test.go:
--------------------------------------------------------------------------------
1 | package internal_test
2 |
3 | import (
4 | "testing"
5 |
6 | . "github.com/onsi/ginkgo/v2"
7 | . "github.com/onsi/gomega"
8 | )
9 |
10 | func TestInternal(t *testing.T) {
11 | RegisterFailHandler(Fail)
12 | RunSpecs(t, "Internal Suite")
13 | }
14 |
--------------------------------------------------------------------------------
/internal/kubernetes/cached/disk/disk_suite_test.go:
--------------------------------------------------------------------------------
1 | package disk_test
2 |
3 | import (
4 | . "github.com/onsi/ginkgo/v2"
5 | . "github.com/onsi/gomega"
6 |
7 | "testing"
8 | )
9 |
10 | func TestDisk(t *testing.T) {
11 | RegisterFailHandler(Fail)
12 | RunSpecs(t, "Disk Suite")
13 | }
14 |
--------------------------------------------------------------------------------
/internal/kubernetes/cached/disk/diskfakes/fake_cache_round_tripper.go:
--------------------------------------------------------------------------------
1 | // Code generated by counterfeiter. DO NOT EDIT.
2 | package diskfakes
3 |
4 | import (
5 | "net/http"
6 | "sync"
7 |
8 | "github.com/homedepot/go-clouddriver/internal/kubernetes/cached/disk"
9 | )
10 |
11 | type FakeCacheRoundTripper struct {
12 | CancelRequestStub func(*http.Request)
13 | cancelRequestMutex sync.RWMutex
14 | cancelRequestArgsForCall []struct {
15 | arg1 *http.Request
16 | }
17 | RoundTripStub func(*http.Request) (*http.Response, error)
18 | roundTripMutex sync.RWMutex
19 | roundTripArgsForCall []struct {
20 | arg1 *http.Request
21 | }
22 | roundTripReturns struct {
23 | result1 *http.Response
24 | result2 error
25 | }
26 | roundTripReturnsOnCall map[int]struct {
27 | result1 *http.Response
28 | result2 error
29 | }
30 | invocations map[string][][]interface{}
31 | invocationsMutex sync.RWMutex
32 | }
33 |
34 | func (fake *FakeCacheRoundTripper) CancelRequest(arg1 *http.Request) {
35 | fake.cancelRequestMutex.Lock()
36 | fake.cancelRequestArgsForCall = append(fake.cancelRequestArgsForCall, struct {
37 | arg1 *http.Request
38 | }{arg1})
39 | stub := fake.CancelRequestStub
40 | fake.recordInvocation("CancelRequest", []interface{}{arg1})
41 | fake.cancelRequestMutex.Unlock()
42 | if stub != nil {
43 | fake.CancelRequestStub(arg1)
44 | }
45 | }
46 |
47 | func (fake *FakeCacheRoundTripper) CancelRequestCallCount() int {
48 | fake.cancelRequestMutex.RLock()
49 | defer fake.cancelRequestMutex.RUnlock()
50 | return len(fake.cancelRequestArgsForCall)
51 | }
52 |
53 | func (fake *FakeCacheRoundTripper) CancelRequestCalls(stub func(*http.Request)) {
54 | fake.cancelRequestMutex.Lock()
55 | defer fake.cancelRequestMutex.Unlock()
56 | fake.CancelRequestStub = stub
57 | }
58 |
59 | func (fake *FakeCacheRoundTripper) CancelRequestArgsForCall(i int) *http.Request {
60 | fake.cancelRequestMutex.RLock()
61 | defer fake.cancelRequestMutex.RUnlock()
62 | argsForCall := fake.cancelRequestArgsForCall[i]
63 | return argsForCall.arg1
64 | }
65 |
66 | func (fake *FakeCacheRoundTripper) RoundTrip(arg1 *http.Request) (*http.Response, error) {
67 | fake.roundTripMutex.Lock()
68 | ret, specificReturn := fake.roundTripReturnsOnCall[len(fake.roundTripArgsForCall)]
69 | fake.roundTripArgsForCall = append(fake.roundTripArgsForCall, struct {
70 | arg1 *http.Request
71 | }{arg1})
72 | stub := fake.RoundTripStub
73 | fakeReturns := fake.roundTripReturns
74 | fake.recordInvocation("RoundTrip", []interface{}{arg1})
75 | fake.roundTripMutex.Unlock()
76 | if stub != nil {
77 | return stub(arg1)
78 | }
79 | if specificReturn {
80 | return ret.result1, ret.result2
81 | }
82 | return fakeReturns.result1, fakeReturns.result2
83 | }
84 |
85 | func (fake *FakeCacheRoundTripper) RoundTripCallCount() int {
86 | fake.roundTripMutex.RLock()
87 | defer fake.roundTripMutex.RUnlock()
88 | return len(fake.roundTripArgsForCall)
89 | }
90 |
91 | func (fake *FakeCacheRoundTripper) RoundTripCalls(stub func(*http.Request) (*http.Response, error)) {
92 | fake.roundTripMutex.Lock()
93 | defer fake.roundTripMutex.Unlock()
94 | fake.RoundTripStub = stub
95 | }
96 |
97 | func (fake *FakeCacheRoundTripper) RoundTripArgsForCall(i int) *http.Request {
98 | fake.roundTripMutex.RLock()
99 | defer fake.roundTripMutex.RUnlock()
100 | argsForCall := fake.roundTripArgsForCall[i]
101 | return argsForCall.arg1
102 | }
103 |
104 | func (fake *FakeCacheRoundTripper) RoundTripReturns(result1 *http.Response, result2 error) {
105 | fake.roundTripMutex.Lock()
106 | defer fake.roundTripMutex.Unlock()
107 | fake.RoundTripStub = nil
108 | fake.roundTripReturns = struct {
109 | result1 *http.Response
110 | result2 error
111 | }{result1, result2}
112 | }
113 |
114 | func (fake *FakeCacheRoundTripper) RoundTripReturnsOnCall(i int, result1 *http.Response, result2 error) {
115 | fake.roundTripMutex.Lock()
116 | defer fake.roundTripMutex.Unlock()
117 | fake.RoundTripStub = nil
118 | if fake.roundTripReturnsOnCall == nil {
119 | fake.roundTripReturnsOnCall = make(map[int]struct {
120 | result1 *http.Response
121 | result2 error
122 | })
123 | }
124 | fake.roundTripReturnsOnCall[i] = struct {
125 | result1 *http.Response
126 | result2 error
127 | }{result1, result2}
128 | }
129 |
130 | func (fake *FakeCacheRoundTripper) Invocations() map[string][][]interface{} {
131 | fake.invocationsMutex.RLock()
132 | defer fake.invocationsMutex.RUnlock()
133 | fake.cancelRequestMutex.RLock()
134 | defer fake.cancelRequestMutex.RUnlock()
135 | fake.roundTripMutex.RLock()
136 | defer fake.roundTripMutex.RUnlock()
137 | copiedInvocations := map[string][][]interface{}{}
138 | for key, value := range fake.invocations {
139 | copiedInvocations[key] = value
140 | }
141 | return copiedInvocations
142 | }
143 |
144 | func (fake *FakeCacheRoundTripper) recordInvocation(key string, args []interface{}) {
145 | fake.invocationsMutex.Lock()
146 | defer fake.invocationsMutex.Unlock()
147 | if fake.invocations == nil {
148 | fake.invocations = map[string][][]interface{}{}
149 | }
150 | if fake.invocations[key] == nil {
151 | fake.invocations[key] = [][]interface{}{}
152 | }
153 | fake.invocations[key] = append(fake.invocations[key], args)
154 | }
155 |
156 | var _ disk.CacheRoundTripper = new(FakeCacheRoundTripper)
157 |
--------------------------------------------------------------------------------
/internal/kubernetes/cached/disk/round_tripper.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2017 The Kubernetes Authors.
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | package disk
18 |
19 | import (
20 | "net/http"
21 | "os"
22 | "path/filepath"
23 |
24 | "github.com/gregjones/httpcache"
25 | "github.com/gregjones/httpcache/diskcache"
26 | "github.com/peterbourgon/diskv"
27 | "k8s.io/klog/v2"
28 | )
29 |
30 | //go:generate counterfeiter . CacheRoundTripper
31 | type CacheRoundTripper interface {
32 | RoundTrip(req *http.Request) (*http.Response, error)
33 | CancelRequest(req *http.Request)
34 | }
35 |
36 | type cacheRoundTripper struct {
37 | rt *httpcache.Transport
38 | }
39 |
40 | func NewCacheRoundTripper(cachDir string, rt http.RoundTripper) http.RoundTripper {
41 | return newCacheRoundTripper(cachDir, rt)
42 | }
43 |
44 | // newCacheRoundTripper creates a roundtripper that reads the ETag on
45 | // response headers and send the If-None-Match header on subsequent
46 | // corresponding requests.
47 | func newCacheRoundTripper(cacheDir string, rt http.RoundTripper) http.RoundTripper {
48 | d := diskv.New(diskv.Options{
49 | PathPerm: os.FileMode(0750),
50 | FilePerm: os.FileMode(0660),
51 | BasePath: cacheDir,
52 | TempDir: filepath.Join(cacheDir, ".diskv-temp"),
53 | })
54 | t := httpcache.NewTransport(diskcache.NewWithDiskv(d))
55 | t.Transport = rt
56 |
57 | return &cacheRoundTripper{rt: t}
58 | }
59 |
60 | func (rt *cacheRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
61 | return rt.rt.RoundTrip(req)
62 | }
63 |
64 | func (rt *cacheRoundTripper) CancelRequest(req *http.Request) {
65 | type canceler interface {
66 | CancelRequest(*http.Request)
67 | }
68 |
69 | if cr, ok := rt.rt.Transport.(canceler); ok {
70 | cr.CancelRequest(req)
71 | } else {
72 | klog.Errorf("CancelRequest not implemented by %T", rt.rt.Transport)
73 | }
74 | }
75 |
76 | func (rt *cacheRoundTripper) WrappedRoundTripper() http.RoundTripper { return rt.rt.Transport }
77 |
--------------------------------------------------------------------------------
/internal/kubernetes/cached/disk/round_tripper_test.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2017 The Kubernetes Authors.
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | package disk_test
18 |
19 | import (
20 | "bytes"
21 | "fmt"
22 | "io"
23 | "net/http"
24 | "net/url"
25 | "os"
26 | "path/filepath"
27 |
28 | . "github.com/homedepot/go-clouddriver/internal/kubernetes/cached/disk"
29 | . "github.com/onsi/ginkgo/v2"
30 | . "github.com/onsi/gomega"
31 | )
32 |
33 | // copied from k8s.io/client-go/transport/round_trippers_test.go
34 | type testRoundTripper struct {
35 | Request *http.Request
36 | Response *http.Response
37 | Err error
38 | }
39 |
40 | func (rt *testRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
41 | rt.Request = req
42 | return rt.Response, rt.Err
43 | }
44 |
45 | var _ = Describe("CachedDiscovery", func() {
46 | var (
47 | rt *testRoundTripper
48 | cacheDir string
49 | err error
50 | cache http.RoundTripper
51 | req *http.Request
52 | resp *http.Response
53 | content []byte
54 | )
55 |
56 | BeforeEach(func() {
57 | rt = &testRoundTripper{}
58 | cacheDir, err = os.MkdirTemp("", "cache-rt")
59 | Expect(err).To(BeNil())
60 | })
61 |
62 | JustBeforeEach(func() {
63 | cache = NewCacheRoundTripper(cacheDir, rt)
64 | })
65 |
66 | AfterEach(func() {
67 | os.RemoveAll(cacheDir)
68 | if resp != nil {
69 | resp.Body.Close()
70 | }
71 | })
72 |
73 | Describe("#RoundTrip", func() {
74 | BeforeEach(func() {
75 | req = &http.Request{
76 | Method: http.MethodGet,
77 | URL: &url.URL{Host: "localhost"},
78 | }
79 | })
80 |
81 | JustBeforeEach(func() {
82 | resp, err = cache.RoundTrip(req)
83 | Expect(err).To(BeNil())
84 | content, err = io.ReadAll(resp.Body)
85 | Expect(err).To(BeNil())
86 | })
87 |
88 | When("the data is not cached", func() {
89 | BeforeEach(func() {
90 | rt.Response = &http.Response{
91 | Header: http.Header{"ETag": []string{`"123456"`}},
92 | Body: io.NopCloser(bytes.NewReader([]byte("Content"))),
93 | StatusCode: http.StatusOK,
94 | }
95 | })
96 |
97 | It("succeeds", func() {
98 | Expect(string(content)).To(Equal("Content"))
99 | })
100 | })
101 |
102 | When("the data is cached", func() {
103 | BeforeEach(func() {
104 | rt.Response = &http.Response{
105 | StatusCode: http.StatusNotModified,
106 | Body: io.NopCloser(bytes.NewReader([]byte("Other Content"))),
107 | }
108 | })
109 |
110 | It("succeeds", func() {
111 | Expect(string(content)).To(Equal("Other Content"))
112 | })
113 | })
114 |
115 | When("the cache directory is removed", func() {
116 | BeforeEach(func() {
117 | os.RemoveAll(cacheDir)
118 | rt.Response = &http.Response{
119 | Header: http.Header{"ETag": []string{`"123456"`}},
120 | Body: io.NopCloser(bytes.NewReader([]byte("Content"))),
121 | StatusCode: http.StatusOK,
122 | }
123 | })
124 |
125 | It("creates the cache directories and files with the correct permissions", func() {
126 | err = filepath.Walk(cacheDir, func(path string, info os.FileInfo, err error) error {
127 | if err != nil {
128 | return err
129 | }
130 | if info.IsDir() {
131 | if info.Mode().Perm() != os.FileMode(0750) {
132 | return fmt.Errorf("directory perm incorrect expected %d got %d", os.FileMode(0750), info.Mode().Perm())
133 | }
134 | } else {
135 | if info.Mode().Perm() != os.FileMode(0660) {
136 | return fmt.Errorf("file perm incorrect expected %d got %d", os.FileMode(0660), info.Mode().Perm())
137 | }
138 | }
139 | return nil
140 | })
141 | Expect(err).To(BeNil())
142 | })
143 | })
144 | })
145 | })
146 |
--------------------------------------------------------------------------------
/internal/kubernetes/cached/memory/memory_suite_test.go:
--------------------------------------------------------------------------------
1 | package memory_test
2 |
3 | import (
4 | "testing"
5 |
6 | . "github.com/onsi/ginkgo/v2"
7 | . "github.com/onsi/gomega"
8 | )
9 |
10 | func TestMemory(t *testing.T) {
11 | RegisterFailHandler(Fail)
12 | RunSpecs(t, "Memory Suite")
13 | }
14 |
--------------------------------------------------------------------------------
/internal/kubernetes/cached/memory/round_tripper.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2017 The Kubernetes Authors.
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | package memory
18 |
19 | import (
20 | "net/http"
21 |
22 | "github.com/gregjones/httpcache"
23 | "k8s.io/klog/v2"
24 | )
25 |
26 | //go:generate counterfeiter . CacheRoundTripper
27 | type MemCacheRoundTripper interface {
28 | RoundTrip(req *http.Request) (*http.Response, error)
29 | CancelRequest(req *http.Request)
30 | }
31 |
32 | type memCacheRoundTripper struct {
33 | rt *httpcache.Transport
34 | }
35 |
36 | func NewMemCacheRoundTripper(rt http.RoundTripper, c *httpcache.MemoryCache) http.RoundTripper {
37 | return newMemCacheRoundTripper(rt, c)
38 | }
39 |
40 | // newMemCacheRoundTripper creates a roundtripper that reads the ETag on
41 | // response headers and send the If-None-Match header on subsequent
42 | // corresponding requests.
43 | func newMemCacheRoundTripper(rt http.RoundTripper, c *httpcache.MemoryCache) http.RoundTripper {
44 | t := httpcache.NewTransport(c)
45 | t.Transport = rt
46 |
47 | return &memCacheRoundTripper{rt: t}
48 | }
49 |
50 | func (rt *memCacheRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
51 | return rt.rt.RoundTrip(req)
52 | }
53 |
54 | func (rt *memCacheRoundTripper) CancelRequest(req *http.Request) {
55 | type canceler interface {
56 | CancelRequest(*http.Request)
57 | }
58 |
59 | if cr, ok := rt.rt.Transport.(canceler); ok {
60 | cr.CancelRequest(req)
61 | } else {
62 | klog.Errorf("CancelRequest not implemented by %T", rt.rt.Transport)
63 | }
64 | }
65 |
66 | func (rt *memCacheRoundTripper) WrappedRoundTripper() http.RoundTripper { return rt.rt.Transport }
67 |
--------------------------------------------------------------------------------
/internal/kubernetes/cached/memory/round_tripper_test.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2017 The Kubernetes Authors.
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | package memory_test
18 |
19 | import (
20 | "bytes"
21 | "io"
22 | "net/http"
23 | "net/url"
24 |
25 | "github.com/gregjones/httpcache"
26 | . "github.com/homedepot/go-clouddriver/internal/kubernetes/cached/memory"
27 | . "github.com/onsi/ginkgo/v2"
28 | . "github.com/onsi/gomega"
29 | )
30 |
31 | // copied from k8s.io/client-go/transport/round_trippers_test.go
32 | type testRoundTripper struct {
33 | Request *http.Request
34 | Response *http.Response
35 | Err error
36 | }
37 |
38 | func (rt *testRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
39 | rt.Request = req
40 | return rt.Response, rt.Err
41 | }
42 |
43 | var _ = Describe("CachedDiscovery", func() {
44 | var (
45 | rt *testRoundTripper
46 | err error
47 | cache http.RoundTripper
48 | httpMemCache *httpcache.MemoryCache
49 | req *http.Request
50 | resp *http.Response
51 | content []byte
52 | )
53 |
54 | BeforeEach(func() {
55 | rt = &testRoundTripper{}
56 | httpMemCache = httpcache.NewMemoryCache()
57 | })
58 |
59 | JustBeforeEach(func() {
60 | cache = NewMemCacheRoundTripper(rt, httpMemCache)
61 | })
62 |
63 | AfterEach(func() {
64 | if resp != nil {
65 | resp.Body.Close()
66 | }
67 | })
68 |
69 | Describe("#RoundTrip", func() {
70 | BeforeEach(func() {
71 | req = &http.Request{
72 | Method: http.MethodGet,
73 | URL: &url.URL{Host: "localhost"},
74 | }
75 | })
76 |
77 | JustBeforeEach(func() {
78 | resp, err = cache.RoundTrip(req)
79 | Expect(err).To(BeNil())
80 | content, err = io.ReadAll(resp.Body)
81 | Expect(err).To(BeNil())
82 | })
83 |
84 | When("the data is not cached", func() {
85 | BeforeEach(func() {
86 | rt.Response = &http.Response{
87 | Header: http.Header{"ETag": []string{`"123456"`}},
88 | Body: io.NopCloser(bytes.NewReader([]byte("Content"))),
89 | StatusCode: http.StatusOK,
90 | }
91 | })
92 |
93 | It("succeeds", func() {
94 | Expect(string(content)).To(Equal("Content"))
95 | })
96 | })
97 |
98 | When("the data is cached", func() {
99 | BeforeEach(func() {
100 | rt.Response = &http.Response{
101 | StatusCode: http.StatusNotModified,
102 | Body: io.NopCloser(bytes.NewReader([]byte("Other Content"))),
103 | }
104 | })
105 |
106 | It("succeeds", func() {
107 | Expect(string(content)).To(Equal("Other Content"))
108 | })
109 | })
110 | })
111 | })
112 |
--------------------------------------------------------------------------------
/internal/kubernetes/clientset.go:
--------------------------------------------------------------------------------
1 | package kubernetes
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "fmt"
7 | "io"
8 | "unicode"
9 |
10 | v1 "k8s.io/api/core/v1"
11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12 | "k8s.io/client-go/kubernetes"
13 | )
14 |
15 | var (
16 | // defaultTailLines sets the logs to get the previous
17 | // 10000 lines.
18 | defaultTailLines = int64(10000)
19 | )
20 |
21 | //go:generate counterfeiter . Clientset
22 | type Clientset interface {
23 | PodLogs(string, string, string) (string, error)
24 | Events(context.Context, string, string, string) ([]v1.Event, error)
25 | }
26 |
27 | type clientset struct {
28 | clientset *kubernetes.Clientset
29 | }
30 |
31 | // PodLogs returns logs for a given container in a given pod in a given
32 | // namespace.
33 | func (c *clientset) PodLogs(name, namespace, container string) (string, error) {
34 | podLogOptions := v1.PodLogOptions{
35 | Container: container,
36 | TailLines: &defaultTailLines,
37 | }
38 |
39 | logs := c.clientset.CoreV1().
40 | Pods(namespace).
41 | GetLogs(name, &podLogOptions)
42 |
43 | stream, err := logs.Stream(context.Background())
44 | if err != nil {
45 | return "", err
46 | }
47 | defer stream.Close()
48 |
49 | buf := new(bytes.Buffer)
50 |
51 | _, err = io.Copy(buf, stream)
52 | if err != nil {
53 | return "", err
54 | }
55 |
56 | return buf.String(), nil
57 | }
58 |
59 | // Events returns events for a given kind, name, and namespace.
60 | func (c *clientset) Events(ctx context.Context, kind, name, namespace string) ([]v1.Event, error) {
61 | lo := metav1.ListOptions{
62 | FieldSelector: fmt.Sprintf("involvedObject.kind=%s,involvedObject.name=%s", uppercaseFirst(kind), name),
63 | }
64 |
65 | events, err := c.clientset.CoreV1().Events(namespace).List(ctx, lo)
66 | if err != nil {
67 | return nil, err
68 | }
69 |
70 | return events.Items, nil
71 | }
72 |
73 | // uppercaseFirst uppercases the first letter of a string.
74 | func uppercaseFirst(str string) string {
75 | for i, v := range str {
76 | return string(unicode.ToUpper(v)) + str[i+1:]
77 | }
78 |
79 | return ""
80 | }
81 |
--------------------------------------------------------------------------------
/internal/kubernetes/cluster.go:
--------------------------------------------------------------------------------
1 | package kubernetes
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | "unicode"
7 | )
8 |
9 | // Generate the cluster that a kind is a part of.
10 | // A Kubernetes cluster is of kind deployment, statefulSet, replicaSet, ingress, service, and daemonSet
11 | // so only generate a cluster for these kinds.
12 | func Cluster(kind, name string) string {
13 | cluster := ""
14 |
15 | if strings.EqualFold(kind, "deployment") ||
16 | strings.EqualFold(kind, "statefulSet") ||
17 | strings.EqualFold(kind, "replicaSet") ||
18 | strings.EqualFold(kind, "ingress") ||
19 | strings.EqualFold(kind, "service") ||
20 | strings.EqualFold(kind, "daemonSet") {
21 | cluster = fmt.Sprintf("%s %s", lowercaseFirst(kind), name)
22 | }
23 |
24 | return cluster
25 | }
26 |
27 | func lowercaseFirst(str string) string {
28 | for i, v := range str {
29 | return string(unicode.ToLower(v)) + str[i+1:]
30 | }
31 |
32 | return ""
33 | }
34 |
--------------------------------------------------------------------------------
/internal/kubernetes/cluster_test.go:
--------------------------------------------------------------------------------
1 | package kubernetes_test
2 |
3 | import (
4 | . "github.com/homedepot/go-clouddriver/internal/kubernetes"
5 | . "github.com/onsi/ginkgo/v2"
6 | . "github.com/onsi/gomega"
7 | )
8 |
9 | var _ = Describe("Cluster", func() {
10 | Describe("#Cluster", func() {
11 | var (
12 | kind string
13 | cluster string
14 | )
15 |
16 | JustBeforeEach(func() {
17 | cluster = Cluster(kind, "test-name")
18 | })
19 |
20 | When("the kind is daemonSet", func() {
21 | BeforeEach(func() {
22 | kind = "DaemonSet"
23 | })
24 |
25 | It("sets the cluster", func() {
26 | Expect(cluster).To(Equal("daemonSet test-name"))
27 | })
28 | })
29 |
30 | When("the kind is deployment", func() {
31 | BeforeEach(func() {
32 | kind = "Deployment"
33 | })
34 |
35 | It("sets the cluster", func() {
36 | Expect(cluster).To(Equal("deployment test-name"))
37 | })
38 | })
39 |
40 | When("the kind is ingress", func() {
41 | BeforeEach(func() {
42 | kind = "Ingress"
43 | })
44 |
45 | It("sets the cluster", func() {
46 | Expect(cluster).To(Equal("ingress test-name"))
47 | })
48 | })
49 |
50 | When("the kind is replicaSet", func() {
51 | BeforeEach(func() {
52 | kind = "ReplicaSet"
53 | })
54 |
55 | It("sets the cluster", func() {
56 | Expect(cluster).To(Equal("replicaSet test-name"))
57 | })
58 | })
59 |
60 | When("the kind is service", func() {
61 | BeforeEach(func() {
62 | kind = "Service"
63 | })
64 |
65 | It("sets the cluster", func() {
66 | Expect(cluster).To(Equal("service test-name"))
67 | })
68 | })
69 |
70 | When("the kind is statefulSet", func() {
71 | BeforeEach(func() {
72 | kind = "StatefulSet"
73 | })
74 |
75 | It("sets the cluster", func() {
76 | Expect(cluster).To(Equal("statefulSet test-name"))
77 | })
78 | })
79 |
80 | When("the kind is not a cluster type", func() {
81 | BeforeEach(func() {
82 | kind = "Pod"
83 | })
84 |
85 | It("does not set the cluster", func() {
86 | Expect(cluster).To(BeEmpty())
87 | })
88 | })
89 | })
90 | })
91 |
--------------------------------------------------------------------------------
/internal/kubernetes/controller.go:
--------------------------------------------------------------------------------
1 | package kubernetes
2 |
3 | import (
4 | "path/filepath"
5 | "regexp"
6 | "strings"
7 | "sync"
8 | "time"
9 |
10 | "github.com/homedepot/go-clouddriver/internal/kubernetes/cached/disk"
11 | "github.com/homedepot/go-clouddriver/internal/kubernetes/cached/memory"
12 | "k8s.io/client-go/dynamic"
13 | "k8s.io/client-go/kubernetes"
14 | "k8s.io/client-go/rest"
15 | "k8s.io/client-go/restmapper"
16 | )
17 |
18 | var (
19 | useDiskCache bool
20 | )
21 |
22 | // Controller holds the ability to generate a new
23 | // dynamic kubernetes client.
24 | //
25 | //go:generate counterfeiter . Controller
26 | type Controller interface {
27 | NewClient(*rest.Config) (Client, error)
28 | NewClientset(*rest.Config) (Clientset, error)
29 | }
30 |
31 | // NewController returns an instance of Controller.
32 | func NewController() Controller {
33 | return &controller{}
34 | }
35 |
36 | type controller struct{}
37 |
38 | // UseDiskCache sets the controller to generate clients that use
39 | // disk cache instead of memory cache for discovery and HTTP responses.
40 | func UseDiskCache() {
41 | useDiskCache = true
42 | }
43 |
44 | // NewClient returns a new dynamic Kubernetes client. By default it returns
45 | // a client that uses in-memory cache store, unless `useDiskCache` is set
46 | // to true. This is where the client stores and references its discovery of
47 | // the Kubernetes API server.
48 | func (c *controller) NewClient(config *rest.Config) (Client, error) {
49 | var (
50 | client Client
51 | err error
52 | )
53 |
54 | if useDiskCache {
55 | client, err = newClientWithDefaultDiskCache(config)
56 | } else {
57 | client, err = newClientWithMemoryCache(config)
58 | }
59 |
60 | return client, err
61 | }
62 |
63 | // NewClientset returns a new kubernetes Clientset wrapper.
64 | func (c *controller) NewClientset(config *rest.Config) (Clientset, error) {
65 | cs, err := kubernetes.NewForConfig(config)
66 | if err != nil {
67 | return nil, err
68 | }
69 |
70 | return &clientset{
71 | clientset: cs,
72 | }, nil
73 | }
74 |
75 | const (
76 | // Default cache directory.
77 | cacheDir = "/var/kube/cache"
78 | defaultTimeout = 180 * time.Second
79 | ttl = 10 * time.Minute
80 | )
81 |
82 | var (
83 | mux sync.Mutex
84 | memCaches = map[string]*memory.Cache{}
85 | )
86 |
87 | func newClientWithMemoryCache(config *rest.Config) (Client, error) {
88 | // If the timeout is not set, set it to the default timeout.
89 | if config.Timeout == 0 {
90 | config.Timeout = defaultTimeout
91 | }
92 |
93 | dynamicClient, err := dynamic.NewForConfig(config)
94 | if err != nil {
95 | return nil, err
96 | }
97 |
98 | mc, err := memCacheClientForConfig(config)
99 | if err != nil {
100 | return nil, err
101 | }
102 |
103 | mapper := restmapper.NewDeferredDiscoveryRESTMapper(mc)
104 | kubeClient := &client{
105 | c: dynamicClient,
106 | config: config,
107 | mapper: mapper,
108 | }
109 |
110 | return kubeClient, nil
111 | }
112 |
113 | func memCacheClientForConfig(inConfig *rest.Config) (memory.CachedDiscoveryClient, error) {
114 | mux.Lock()
115 | defer mux.Unlock()
116 |
117 | config := inConfig
118 | if _, ok := memCaches[config.Host]; !ok {
119 | memCaches[config.Host] = memory.NewCache(ttl)
120 | }
121 |
122 | memCache := memCaches[config.Host]
123 |
124 | return memCache.NewClientForConfig(config)
125 | }
126 |
127 | func newClientWithDefaultDiskCache(config *rest.Config) (Client, error) {
128 | // If the timeout is not set, set it to the default timeout.
129 | if config.Timeout == 0 {
130 | config.Timeout = defaultTimeout
131 | }
132 |
133 | dynamicClient, err := dynamic.NewForConfig(config)
134 | if err != nil {
135 | return nil, err
136 | }
137 |
138 | // Some code to define this take from
139 | // https://github.com/kubernetes/cli-runtime/blob/master/pkg/genericclioptions/config_flags.go#L215
140 | httpCacheDir := filepath.Join(cacheDir, "http")
141 | discoveryCacheDir := computeDiscoverCacheDir(filepath.Join(cacheDir, "discovery"), config.Host)
142 |
143 | // DiscoveryClient queries API server about the resources
144 | cdc, err := disk.NewCachedDiscoveryClientForConfig(config, discoveryCacheDir, httpCacheDir, ttl)
145 | if err != nil {
146 | return nil, err
147 | }
148 |
149 | mapper := restmapper.NewDeferredDiscoveryRESTMapper(cdc)
150 | kubeClient := &client{
151 | c: dynamicClient,
152 | config: config,
153 | mapper: mapper,
154 | }
155 |
156 | return kubeClient, nil
157 | }
158 |
159 | // overlyCautiousIllegalFileCharacters matches characters that *might* not be supported.
160 | // Windows is really restrictive, so this is really restrictive.
161 | var overlyCautiousIllegalFileCharacters = regexp.MustCompile(`[^(\w/\.)]`)
162 |
163 | // computeDiscoverCacheDir takes the parentDir and the host and comes up with a "usually non-colliding" name.
164 | func computeDiscoverCacheDir(parentDir, host string) string {
165 | // strip the optional scheme from host if its there:
166 | schemelessHost := strings.Replace(strings.Replace(host, "https://", "", 1), "http://", "", 1)
167 | // now do a simple collapse of non-AZ09 characters. Collisions are possible but unlikely.
168 | // Even if we do collide the problem is short lived
169 | safeHost := overlyCautiousIllegalFileCharacters.ReplaceAllString(schemelessHost, "_")
170 |
171 | return filepath.Join(parentDir, safeHost)
172 | }
173 |
--------------------------------------------------------------------------------
/internal/kubernetes/custom_kind.go:
--------------------------------------------------------------------------------
1 | package kubernetes
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "os"
7 | "strings"
8 | "sync"
9 |
10 | "github.com/homedepot/go-clouddriver/internal/kubernetes/manifest"
11 | clouddriver "github.com/homedepot/go-clouddriver/pkg"
12 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
13 | )
14 |
15 | var (
16 | once sync.Once
17 | configFile map[string]CustomKindConfig
18 | )
19 |
20 | // CustomKindConfig describes the structure of each item in the custom kinds config file, see example below:
21 | //
22 | // {
23 | // "myCustomKind": {
24 | // "statusChecks": [
25 | // {
26 | // "fieldPath": "field1.field2",
27 | // "comparedValue": true,
28 | // "operator": "EQ"
29 | // }
30 | // ]
31 | // }
32 | // }
33 | type CustomKindConfig struct {
34 | StatusChecks []StatusCheck `json:"statusChecks"`
35 | }
36 |
37 | type StatusCheck struct {
38 | // The path to the field within the manifest's status object that the status check should evaluate,
39 | // use dot notation for nested fields
40 | FieldPath string `json:"fieldPath"`
41 | ComparedValue interface{} `json:"comparedValue"`
42 | // Specifies how to compare the actual value and the compared value;
43 | // the status check passes if the comparison evaluates to true and fails otherwise.
44 | // Currently only supports EQ and NE
45 | Operator string `json:"operator"`
46 | }
47 |
48 | type CustomKind struct {
49 | CustomKindConfig
50 | manifest *unstructured.Unstructured
51 | }
52 |
53 | func NewCustomKind(kind string, m map[string]interface{}) *CustomKind {
54 | manifest, err := ToUnstructured(m)
55 | if err != nil {
56 | clouddriver.Log(fmt.Errorf("error creating unstructured object from manifest: %v", err))
57 | }
58 |
59 | configData := getCustomKindConfig(kind)
60 |
61 | return &CustomKind{manifest: &manifest, CustomKindConfig: configData}
62 | }
63 |
64 | func (k *CustomKind) Object() *unstructured.Unstructured {
65 | return k.manifest
66 | }
67 |
68 | func (k *CustomKind) Status() manifest.Status {
69 | s := manifest.DefaultStatus
70 |
71 | unstructuredContent := k.manifest.UnstructuredContent()
72 | if _, ok := unstructuredContent["status"]; !ok {
73 | return s
74 | }
75 |
76 | statusData := unstructuredContent["status"].(map[string]interface{})
77 |
78 | for _, statusCheck := range k.StatusChecks {
79 | statusValue := getStatusValue(statusData, statusCheck.FieldPath)
80 | if statusValue == nil {
81 | continue
82 | }
83 |
84 | if !evaluateStatusCheck(statusValue, statusCheck.ComparedValue, statusCheck.Operator) {
85 | s.Stable.State = false
86 | s.Failed.State = true
87 | s.Failed.Message = fmt.Sprintf("Field status.%s was %v", statusCheck.FieldPath, statusValue)
88 |
89 | return s
90 | }
91 | }
92 |
93 | return s
94 | }
95 |
96 | func getCustomKindConfig(kind string) CustomKindConfig {
97 | customKindsConfigPath := os.Getenv("CUSTOM_KINDS_CONFIG_PATH")
98 | if customKindsConfigPath == "" {
99 | return CustomKindConfig{}
100 | }
101 |
102 | if configFile == nil {
103 | once.Do(func() {
104 | allConfigs := make(map[string]CustomKindConfig)
105 | configBytes, err := os.ReadFile(customKindsConfigPath)
106 |
107 | if err != nil {
108 | clouddriver.Log(fmt.Errorf("error reading custom kinds config file at %s: %v",
109 | customKindsConfigPath, err))
110 | }
111 |
112 | if err := json.Unmarshal(configBytes, &allConfigs); err != nil {
113 | clouddriver.Log(fmt.Errorf("error setting up custom kinds config: %v", err))
114 | }
115 |
116 | configFile = allConfigs
117 | })
118 | }
119 |
120 | config, ok := configFile[kind]
121 | if !ok {
122 | return CustomKindConfig{}
123 | }
124 |
125 | return config
126 | }
127 |
128 | func getStatusValue(statusMap map[string]interface{}, fieldPath string) interface{} {
129 | fields := strings.Split(fieldPath, ".")
130 | if len(fields) == 0 {
131 | return nil
132 | }
133 |
134 | currField := fields[0]
135 |
136 | if len(fields) == 1 {
137 | val, exists := statusMap[currField]
138 | if !exists {
139 | return nil
140 | }
141 |
142 | return val
143 | }
144 |
145 | remainingFields := fields[1:]
146 |
147 | val, exists := statusMap[currField]
148 | if !exists {
149 | return nil
150 | }
151 |
152 | // recursively traverses the status object until we reach the field we're looking for
153 | return getStatusValue(val.(map[string]interface{}), strings.Join(remainingFields, "."))
154 | }
155 |
156 | func evaluateStatusCheck(actual, compared interface{}, operator string) bool {
157 | // we can add more operators if necessary
158 | switch strings.ToLower(operator) {
159 | case "eq":
160 | return actual == compared
161 | case "ne":
162 | return actual != compared
163 | default:
164 | return true
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/internal/kubernetes/custom_kind_test.go:
--------------------------------------------------------------------------------
1 | package kubernetes_test
2 |
3 | import (
4 | "encoding/json"
5 | "os"
6 |
7 | . "github.com/homedepot/go-clouddriver/internal/kubernetes"
8 | "github.com/homedepot/go-clouddriver/internal/kubernetes/manifest"
9 | . "github.com/onsi/ginkgo/v2"
10 | . "github.com/onsi/gomega"
11 | )
12 |
13 | var _ = Describe("Custom Kind", Ordered, func() {
14 | var (
15 | customKind *CustomKind
16 | fakeCustomKindConfig map[string]CustomKindConfig
17 | )
18 |
19 | BeforeAll(func() {
20 | fakeCustomKindConfig = map[string]CustomKindConfig{
21 | "test": {
22 | StatusChecks: []StatusCheck{
23 | {
24 | FieldPath: "phase.type",
25 | ComparedValue: "Error",
26 | Operator: "NE",
27 | },
28 | },
29 | },
30 | }
31 | customKindsConfigPath := "./customKindsConfig.json"
32 | os.Setenv("CUSTOM_KINDS_CONFIG_PATH", customKindsConfigPath)
33 | f, err := os.Create(customKindsConfigPath)
34 | Expect(err).To(BeNil())
35 | err = json.NewEncoder(f).Encode(fakeCustomKindConfig)
36 | Expect(err).To(BeNil())
37 | f.Close()
38 | })
39 |
40 | AfterAll(func() {
41 | os.Remove(os.Getenv("CUSTOM_KINDS_CONFIG_PATH"))
42 | })
43 |
44 | Describe("#Object", func() {
45 | BeforeEach(func() {
46 | fakeManifest := map[string]interface{}{"status": "1", "kind": "test"}
47 | customKind = NewCustomKind("test", fakeManifest)
48 | })
49 |
50 | When("it succeeds", func() {
51 | It("succeeds", func() {
52 | Expect(customKind.Object()).ToNot(BeNil())
53 | })
54 | })
55 | })
56 |
57 | Describe("#Status", func() {
58 | var s manifest.Status
59 |
60 | JustBeforeEach(func() {
61 | s = customKind.Status()
62 | })
63 |
64 | When("there is no configuration for the kind", func() {
65 | BeforeEach(func() {
66 | status := map[string]interface{}{"ready": false}
67 | fakeManifest := map[string]interface{}{"status": status, "kind": "doesnotexist"}
68 | customKind = NewCustomKind("doesnotexist", fakeManifest)
69 | })
70 |
71 | It("returns default status", func() {
72 | Expect(s).To(Equal(manifest.DefaultStatus))
73 | })
74 | })
75 |
76 | When("the status check fails", func() {
77 | BeforeEach(func() {
78 | status := map[string]interface{}{"phase": map[string]interface{}{"type": "Error", "reason": "some reason"}}
79 | fakeManifest := map[string]interface{}{"status": status, "kind": "test"}
80 | customKind = NewCustomKind("test", fakeManifest)
81 | })
82 |
83 | It("returns status failed", func() {
84 | Expect(s.Failed.State).To(BeTrue())
85 | Expect(s.Failed.Message).To(Equal("Field status.phase.type was Error"))
86 | })
87 | })
88 |
89 | When("the expected field does not exist", func() {
90 | BeforeEach(func() {
91 | fakeManifest := map[string]interface{}{"randomField": "1", "kind": "test"}
92 | customKind = NewCustomKind("test", fakeManifest)
93 | })
94 |
95 | It("returns default status", func() {
96 | Expect(s).To(Equal(manifest.DefaultStatus))
97 | })
98 | })
99 |
100 | When("the status check succeeds", func() {
101 | BeforeEach(func() {
102 | status := map[string]interface{}{"phase": map[string]interface{}{"type": "some type"}}
103 | fakeManifest := map[string]interface{}{"status": status, "kind": "test"}
104 | customKind = NewCustomKind("test", fakeManifest)
105 | })
106 |
107 | It("returns the expected status", func() {
108 | Expect(s.Stable.State).To(BeTrue())
109 | Expect(s.Failed.State).To(BeFalse())
110 | })
111 | })
112 | })
113 | })
114 |
--------------------------------------------------------------------------------
/internal/kubernetes/daemonset.go:
--------------------------------------------------------------------------------
1 | package kubernetes
2 |
3 | import (
4 | "encoding/json"
5 | "reflect"
6 | "strings"
7 |
8 | "github.com/homedepot/go-clouddriver/internal/kubernetes/manifest"
9 | v1 "k8s.io/api/apps/v1"
10 | )
11 |
12 | func NewDaemonSet(m map[string]interface{}) *DaemonSet {
13 | ds := &v1.DaemonSet{}
14 | b, _ := json.Marshal(m)
15 | _ = json.Unmarshal(b, &ds)
16 |
17 | return &DaemonSet{ds: ds}
18 | }
19 |
20 | type DaemonSet struct {
21 | ds *v1.DaemonSet
22 | }
23 |
24 | func (ds *DaemonSet) Object() *v1.DaemonSet {
25 | return ds.ds
26 | }
27 |
28 | func (ds *DaemonSet) Status() manifest.Status {
29 | s := manifest.DefaultStatus
30 |
31 | if reflect.DeepEqual(ds.ds.Status, v1.DaemonSetStatus{}) {
32 | s = manifest.NoneReported
33 |
34 | return s
35 | }
36 |
37 | if strings.EqualFold(string(ds.ds.Spec.UpdateStrategy.Type), "rollingupdate") {
38 | return s
39 | }
40 |
41 | if ds.ds.ObjectMeta.Generation != ds.ds.Status.ObservedGeneration {
42 | s.Stable.State = false
43 | s.Stable.Message = "Waiting for status generation to match updated object generation"
44 |
45 | return s
46 | }
47 |
48 | desiredReplicas := ds.ds.Status.DesiredNumberScheduled
49 |
50 | {
51 | scheduledReplicas := ds.ds.Status.CurrentNumberScheduled
52 | if scheduledReplicas < desiredReplicas {
53 | s.Stable.State = false
54 | s.Stable.Message = "Waiting for all replicas to be scheduled"
55 |
56 | return s
57 | }
58 | }
59 |
60 | {
61 | updatedReplicas := ds.ds.Status.UpdatedNumberScheduled
62 | if updatedReplicas < desiredReplicas {
63 | s.Stable.State = false
64 | s.Stable.Message = "Waiting for all updated replicas to be scheduled"
65 |
66 | return s
67 | }
68 | }
69 |
70 | {
71 | availableReplicas := ds.ds.Status.NumberAvailable
72 | if availableReplicas < desiredReplicas {
73 | s.Stable.State = false
74 | s.Stable.Message = "Waiting for all replicas to be available"
75 |
76 | return s
77 | }
78 | }
79 |
80 | {
81 | readyReplicas := ds.ds.Status.NumberReady
82 | if readyReplicas < desiredReplicas {
83 | s.Stable.State = false
84 | s.Stable.Message = "Waiting for all replicas to be ready"
85 |
86 | return s
87 | }
88 | }
89 |
90 | return s
91 | }
92 |
--------------------------------------------------------------------------------
/internal/kubernetes/daemonset_test.go:
--------------------------------------------------------------------------------
1 | package kubernetes_test
2 |
3 | import (
4 | . "github.com/homedepot/go-clouddriver/internal/kubernetes"
5 | "github.com/homedepot/go-clouddriver/internal/kubernetes/manifest"
6 | . "github.com/onsi/ginkgo/v2"
7 | . "github.com/onsi/gomega"
8 | v1 "k8s.io/api/apps/v1"
9 | )
10 |
11 | var _ = Describe("DaemonSet", func() {
12 | var (
13 | daemonSet *DaemonSet
14 | )
15 |
16 | BeforeEach(func() {
17 | ds := map[string]interface{}{}
18 | daemonSet = NewDaemonSet(ds)
19 | })
20 |
21 | Describe("#Status", func() {
22 | var s manifest.Status
23 |
24 | BeforeEach(func() {
25 | daemonSet.Object().Status.DesiredNumberScheduled = 4
26 | })
27 |
28 | JustBeforeEach(func() {
29 | s = daemonSet.Status()
30 | })
31 |
32 | When("there is no status", func() {
33 | BeforeEach(func() {
34 | ds := map[string]interface{}{}
35 | daemonSet = NewDaemonSet(ds)
36 | })
37 |
38 | It("returns status unstable and unavailable", func() {
39 | Expect(s.Stable.State).To(BeFalse())
40 | Expect(s.Stable.Message).To(Equal("No status reported yet"))
41 | Expect(s.Available.State).To(BeFalse())
42 | Expect(s.Available.Message).To(Equal("No availability reported"))
43 |
44 | })
45 | })
46 |
47 | When("the update stategy is rolling update", func() {
48 | BeforeEach(func() {
49 | o := daemonSet.Object()
50 | o.Spec.UpdateStrategy = v1.DaemonSetUpdateStrategy{
51 | Type: v1.RollingUpdateDaemonSetStrategyType,
52 | }
53 | })
54 |
55 | It("returns status stable and available", func() {
56 | Expect(s.Stable.State).To(BeTrue())
57 | Expect(s.Available.State).To(BeTrue())
58 | })
59 | })
60 |
61 | When("the generations do not match", func() {
62 | BeforeEach(func() {
63 | o := daemonSet.Object()
64 | o.ObjectMeta.Generation = 100
65 | o.Status.ObservedGeneration = 99
66 | })
67 |
68 | It("returns status unstable", func() {
69 | Expect(s.Stable.State).To(BeFalse())
70 | Expect(s.Stable.Message).To(Equal("Waiting for status generation to match updated object generation"))
71 | })
72 | })
73 |
74 | When("scheduled replicas is less than desired", func() {
75 | BeforeEach(func() {
76 | o := daemonSet.Object()
77 | o.Status.CurrentNumberScheduled = int32(2)
78 | })
79 |
80 | It("returns the expected status", func() {
81 | Expect(s.Stable.State).To(BeFalse())
82 | Expect(s.Stable.Message).To(Equal("Waiting for all replicas to be scheduled"))
83 | })
84 | })
85 |
86 | When("updated replicas is less than desired", func() {
87 | BeforeEach(func() {
88 | o := daemonSet.Object()
89 | o.Status.CurrentNumberScheduled = int32(4)
90 | o.Status.UpdatedNumberScheduled = int32(2)
91 | })
92 |
93 | It("returns the expected status", func() {
94 | Expect(s.Stable.State).To(BeFalse())
95 | Expect(s.Stable.Message).To(Equal("Waiting for all updated replicas to be scheduled"))
96 | })
97 | })
98 |
99 | When("there are less available replicas than desireds replicas", func() {
100 | BeforeEach(func() {
101 | o := daemonSet.Object()
102 | o.Status.CurrentNumberScheduled = int32(4)
103 | o.Status.UpdatedNumberScheduled = int32(4)
104 | o.Status.NumberAvailable = int32(2)
105 | })
106 |
107 | It("returns the expected status", func() {
108 | Expect(s.Stable.State).To(BeFalse())
109 | Expect(s.Stable.Message).To(Equal("Waiting for all replicas to be available"))
110 | })
111 | })
112 |
113 | When("there are less ready replicas than desireds replicas", func() {
114 | BeforeEach(func() {
115 | o := daemonSet.Object()
116 | o.Status.CurrentNumberScheduled = int32(4)
117 | o.Status.UpdatedNumberScheduled = int32(4)
118 | o.Status.NumberAvailable = int32(4)
119 | o.Status.NumberReady = int32(2)
120 | })
121 |
122 | It("returns the expected status", func() {
123 | Expect(s.Stable.State).To(BeFalse())
124 | Expect(s.Stable.Message).To(Equal("Waiting for all replicas to be ready"))
125 | })
126 | })
127 |
128 | When("it succeeds", func() {
129 | BeforeEach(func() {
130 | o := daemonSet.Object()
131 | o.Status.CurrentNumberScheduled = int32(4)
132 | o.Status.UpdatedNumberScheduled = int32(4)
133 | o.Status.NumberAvailable = int32(4)
134 | o.Status.NumberReady = int32(4)
135 | })
136 |
137 | It("returns the expected status", func() {
138 | Expect(s.Stable.State).To(BeTrue())
139 | })
140 | })
141 | })
142 | })
143 |
--------------------------------------------------------------------------------
/internal/kubernetes/deployment.go:
--------------------------------------------------------------------------------
1 | package kubernetes
2 |
3 | import (
4 | "encoding/json"
5 | "strings"
6 |
7 | "github.com/homedepot/go-clouddriver/internal/kubernetes/manifest"
8 | v1 "k8s.io/api/apps/v1"
9 | )
10 |
11 | func NewDeployment(m map[string]interface{}) *Deployment {
12 | d := &v1.Deployment{}
13 | b, _ := json.Marshal(m)
14 | _ = json.Unmarshal(b, &d)
15 |
16 | return &Deployment{d: d}
17 | }
18 |
19 | type Deployment struct {
20 | d *v1.Deployment
21 | }
22 |
23 | func (d *Deployment) Object() *v1.Deployment {
24 | return d.d
25 | }
26 |
27 | func (d *Deployment) Status() manifest.Status {
28 | s := manifest.DefaultStatus
29 |
30 | if d.d.ObjectMeta.Generation != d.d.Status.ObservedGeneration {
31 | s.Stable.State = false
32 | s.Stable.Message = "Waiting for status generation to match updated object generation"
33 |
34 | return s
35 | }
36 |
37 | conditions := d.d.Status.Conditions
38 | for _, condition := range conditions {
39 | if strings.EqualFold(condition.Reason, "deploymentpaused") {
40 | s.Paused.State = true
41 | }
42 |
43 | if strings.EqualFold(string(condition.Type), "available") &&
44 | strings.EqualFold(string(condition.Status), "false") {
45 | s.Available.State = false
46 | s.Available.Message = condition.Reason
47 | s.Stable.State = false
48 | s.Stable.Message = condition.Reason
49 | }
50 |
51 | if strings.EqualFold(string(condition.Type), "progressing") &&
52 | strings.EqualFold(condition.Reason, "progressdeadlineexceeded") {
53 | s.Failed.State = true
54 | s.Failed.Message = condition.Message
55 | }
56 | }
57 |
58 | desiredReplicas := int32(0)
59 |
60 | if d.d.Spec.Replicas != nil {
61 | desiredReplicas = *d.d.Spec.Replicas
62 | }
63 |
64 | {
65 | updatedReplicas := d.d.Status.UpdatedReplicas
66 | if updatedReplicas < desiredReplicas {
67 | s.Stable.State = false
68 | s.Stable.Message = "Waiting for all replicas to be updated"
69 |
70 | return s
71 | }
72 |
73 | statusReplicas := d.d.Status.Replicas
74 | if statusReplicas > updatedReplicas {
75 | s.Stable.State = false
76 | s.Stable.Message = "Waiting for old replicas to finish termination"
77 |
78 | return s
79 | }
80 | }
81 |
82 | {
83 | availableReplicas := d.d.Status.AvailableReplicas
84 | if availableReplicas < desiredReplicas {
85 | s.Stable.State = false
86 | s.Stable.Message = "Waiting for all replicas to be available"
87 |
88 | return s
89 | }
90 | }
91 |
92 | {
93 | readyReplicas := d.d.Status.ReadyReplicas
94 | if readyReplicas < desiredReplicas {
95 | s.Stable.State = false
96 | s.Stable.Message = "Waiting for all replicas to be ready"
97 |
98 | return s
99 | }
100 | }
101 |
102 | return s
103 | }
104 |
--------------------------------------------------------------------------------
/internal/kubernetes/filter.go:
--------------------------------------------------------------------------------
1 | package kubernetes
2 |
3 | import (
4 | "strings"
5 |
6 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
7 | )
8 |
9 | // FilterOnAnnotations takes a slice of unstructured and returns
10 | // the filtered slice based on a given annotation key and value.
11 | func FilterOnAnnotation(items []unstructured.Unstructured,
12 | annotationKey, annotationValue string) []unstructured.Unstructured {
13 | filtered := []unstructured.Unstructured{}
14 |
15 | for _, item := range items {
16 | annotations := item.GetAnnotations()
17 | if annotations != nil &&
18 | strings.EqualFold(annotations[annotationKey], annotationValue) {
19 | filtered = append(filtered, item)
20 | }
21 | }
22 |
23 | return filtered
24 | }
25 |
26 | // FilterOnLabelExists takes a slice of unstructured and returns
27 | // a filtered slice where a given label exists in each
28 | // of the unstructured objects.
29 | func FilterOnLabelExists(items []unstructured.Unstructured,
30 | label string) []unstructured.Unstructured {
31 | filtered := []unstructured.Unstructured{}
32 |
33 | for _, item := range items {
34 | labels := item.GetLabels()
35 | if labels != nil {
36 | if _, ok := labels[label]; ok {
37 | filtered = append(filtered, item)
38 | }
39 | }
40 | }
41 |
42 | return filtered
43 | }
44 |
--------------------------------------------------------------------------------
/internal/kubernetes/horizontalpodautoscaler.go:
--------------------------------------------------------------------------------
1 | package kubernetes
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 |
7 | "github.com/homedepot/go-clouddriver/internal/kubernetes/manifest"
8 | v1 "k8s.io/api/autoscaling/v1"
9 | )
10 |
11 | func NewHorizontalPodAutoscaler(m map[string]interface{}) *HorizontalPodAutoscaler {
12 | hpa := &v1.HorizontalPodAutoscaler{}
13 | b, _ := json.Marshal(m)
14 | _ = json.Unmarshal(b, &hpa)
15 |
16 | return &HorizontalPodAutoscaler{hpa: hpa}
17 | }
18 |
19 | type HorizontalPodAutoscaler struct {
20 | hpa *v1.HorizontalPodAutoscaler
21 | }
22 |
23 | func (hpa *HorizontalPodAutoscaler) Object() *v1.HorizontalPodAutoscaler {
24 | return hpa.hpa
25 | }
26 |
27 | func (hpa *HorizontalPodAutoscaler) Status() manifest.Status {
28 | s := manifest.DefaultStatus
29 |
30 | hpaStatus := hpa.hpa.Status
31 | if hpaStatus.DesiredReplicas > hpaStatus.CurrentReplicas {
32 | s.Stable.State = false
33 | s.Stable.Message = fmt.Sprintf("Waiting for HPA to complete a scale up, current: %d desired: %d", hpaStatus.CurrentReplicas, hpaStatus.DesiredReplicas)
34 | s.Available.State = false
35 | s.Available.Message = fmt.Sprintf("Waiting for HPA to complete a scale up, current: %d desired: %d", hpaStatus.CurrentReplicas, hpaStatus.DesiredReplicas)
36 | }
37 |
38 | if hpaStatus.DesiredReplicas < hpaStatus.CurrentReplicas {
39 | s.Stable.State = false
40 | s.Stable.Message = fmt.Sprintf("Waiting for HPA to complete a scale down, current: %d desired: %d", hpaStatus.CurrentReplicas, hpaStatus.DesiredReplicas)
41 | s.Available.State = false
42 | s.Available.Message = fmt.Sprintf("Waiting for HPA to complete a scale down, current: %d desired: %d", hpaStatus.CurrentReplicas, hpaStatus.DesiredReplicas)
43 | }
44 |
45 | return s
46 | }
47 |
--------------------------------------------------------------------------------
/internal/kubernetes/horizontalpodautoscaler_test.go:
--------------------------------------------------------------------------------
1 | package kubernetes_test
2 |
3 | import (
4 | . "github.com/onsi/ginkgo/v2"
5 | . "github.com/onsi/gomega"
6 |
7 | . "github.com/homedepot/go-clouddriver/internal/kubernetes"
8 | "github.com/homedepot/go-clouddriver/internal/kubernetes/manifest"
9 | )
10 |
11 | var _ = Describe("HorizontalPodAutoscaler", func() {
12 | var (
13 | hpa *HorizontalPodAutoscaler
14 | )
15 |
16 | BeforeEach(func() {
17 | hpa = NewHorizontalPodAutoscaler(map[string]interface{}{})
18 | })
19 |
20 | Describe("#Status", func() {
21 | var s manifest.Status
22 |
23 | JustBeforeEach(func() {
24 | s = hpa.Status()
25 | })
26 |
27 | When("DesiredReplicas > CurrentReplicas", func() {
28 | BeforeEach(func() {
29 | o := hpa.Object()
30 | o.Status.DesiredReplicas = 5
31 | o.Status.CurrentReplicas = 3
32 | })
33 |
34 | It("returns expected status", func() {
35 | Expect(s.Stable.State).To(BeFalse())
36 | Expect(s.Stable.Message).To(Equal("Waiting for HPA to complete a scale up, current: 3 desired: 5"))
37 | Expect(s.Available.State).To(BeFalse())
38 | Expect(s.Available.Message).To(Equal("Waiting for HPA to complete a scale up, current: 3 desired: 5"))
39 | })
40 | })
41 |
42 | When("DesiredReplicas < CurrentReplicas", func() {
43 | BeforeEach(func() {
44 | o := hpa.Object()
45 | o.Status.DesiredReplicas = 3
46 | o.Status.CurrentReplicas = 5
47 | })
48 |
49 | It("returns expected status", func() {
50 | Expect(s.Stable.State).To(BeFalse())
51 | Expect(s.Stable.Message).To(Equal("Waiting for HPA to complete a scale down, current: 5 desired: 3"))
52 | Expect(s.Available.State).To(BeFalse())
53 | Expect(s.Available.Message).To(Equal("Waiting for HPA to complete a scale down, current: 5 desired: 3"))
54 | })
55 | })
56 | })
57 | })
58 |
--------------------------------------------------------------------------------
/internal/kubernetes/job.go:
--------------------------------------------------------------------------------
1 | package kubernetes
2 |
3 | import (
4 | "encoding/json"
5 |
6 | "github.com/homedepot/go-clouddriver/internal/kubernetes/manifest"
7 | v1 "k8s.io/api/batch/v1"
8 | )
9 |
10 | func NewJob(m map[string]interface{}) *Job {
11 | j := &v1.Job{}
12 | b, _ := json.Marshal(m)
13 | _ = json.Unmarshal(b, &j)
14 |
15 | return &Job{j: j}
16 | }
17 |
18 | type Job struct {
19 | j *v1.Job
20 | }
21 |
22 | func (j *Job) Object() *v1.Job {
23 | return j.j
24 | }
25 |
26 | // Calculated at https://github.com/spinnaker/clouddriver/blob/master/clouddriver-kubernetes/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/model/KubernetesJobStatus.java#L71
27 | func (j *Job) State() string {
28 | obj := j.Object()
29 | status := obj.Status
30 |
31 | if status.CompletionTime == nil {
32 | return "Running"
33 | }
34 |
35 | completions := int32(1)
36 | if obj.Spec.Completions != nil {
37 | completions = *obj.Spec.Completions
38 | }
39 |
40 | succeeded := status.Succeeded
41 |
42 | if succeeded < completions {
43 | conditions := status.Conditions
44 | failed := false
45 |
46 | for _, condition := range conditions {
47 | if condition.Type == v1.JobFailed {
48 | failed = true
49 | break
50 | }
51 | }
52 |
53 | if failed {
54 | return "Failed"
55 | }
56 |
57 | return "Running"
58 | }
59 |
60 | return "Succeeded"
61 | }
62 |
63 | func (j *Job) Status() manifest.Status {
64 | s := manifest.DefaultStatus
65 |
66 | completions := int32(1)
67 | spec := j.j.Spec
68 | status := j.j.Status
69 |
70 | if spec.Completions != nil {
71 | completions = *spec.Completions
72 | }
73 |
74 | succeeded := status.Succeeded
75 | if succeeded < completions {
76 | conditions := status.Conditions
77 | for _, condition := range conditions {
78 | if condition.Type == v1.JobFailed {
79 | s.Failed.State = true
80 | s.Failed.Message = condition.Message
81 |
82 | return s
83 | }
84 | }
85 |
86 | s.Stable.State = false
87 | s.Stable.Message = "Waiting for jobs to finish"
88 | }
89 |
90 | return s
91 | }
92 |
--------------------------------------------------------------------------------
/internal/kubernetes/job_test.go:
--------------------------------------------------------------------------------
1 | package kubernetes_test
2 |
3 | import (
4 | . "github.com/onsi/ginkgo/v2"
5 | . "github.com/onsi/gomega"
6 | v1 "k8s.io/api/batch/v1"
7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8 |
9 | . "github.com/homedepot/go-clouddriver/internal/kubernetes"
10 | "github.com/homedepot/go-clouddriver/internal/kubernetes/manifest"
11 | )
12 |
13 | var _ = Describe("Job", func() {
14 | var (
15 | job *Job
16 | )
17 |
18 | BeforeEach(func() {
19 | j := map[string]interface{}{}
20 | job = NewJob(j)
21 | })
22 |
23 | Describe("#State", func() {
24 | var s string
25 |
26 | JustBeforeEach(func() {
27 | s = job.State()
28 | })
29 |
30 | When("the job has not completed", func() {
31 | BeforeEach(func() {
32 | o := job.Object()
33 | o.Status.CompletionTime = nil
34 | })
35 |
36 | It("returns expected state", func() {
37 | Expect(s).To(Equal("Running"))
38 | })
39 | })
40 |
41 | When("the job has failed", func() {
42 | BeforeEach(func() {
43 | completions := int32(1)
44 | o := job.Object()
45 | o.Status.CompletionTime = &metav1.Time{}
46 | o.Spec.Completions = &completions
47 | o.Status.Conditions = []v1.JobCondition{
48 | {
49 | Type: "Failed",
50 | },
51 | }
52 | })
53 |
54 | It("returns expected state", func() {
55 | Expect(s).To(Equal("Failed"))
56 | })
57 | })
58 |
59 | When("the job is partially successful", func() {
60 | BeforeEach(func() {
61 | completions := int32(1)
62 | o := job.Object()
63 | o.Status.CompletionTime = &metav1.Time{}
64 | o.Spec.Completions = &completions
65 | })
66 |
67 | It("returns expected state", func() {
68 | Expect(s).To(Equal("Running"))
69 | })
70 | })
71 |
72 | When("the job succeeded", func() {
73 | BeforeEach(func() {
74 | completions := int32(1)
75 | o := job.Object()
76 | o.Status.CompletionTime = &metav1.Time{}
77 | o.Status.Succeeded = int32(1)
78 | o.Spec.Completions = &completions
79 | })
80 |
81 | It("returns expected state", func() {
82 | Expect(s).To(Equal("Succeeded"))
83 | })
84 | })
85 | })
86 |
87 | Describe("#Status", func() {
88 | var s manifest.Status
89 |
90 | BeforeEach(func() {
91 | completions := int32(1)
92 | o := job.Object()
93 | o.Status.Succeeded = 1
94 | o.Spec.Completions = &completions
95 | })
96 |
97 | JustBeforeEach(func() {
98 | s = job.Status()
99 | })
100 |
101 | Context("succeeded pods is less than completions", func() {
102 | BeforeEach(func() {
103 | o := job.Object()
104 | completions := int32(2)
105 | o.Status.Succeeded = 1
106 | o.Spec.Completions = &completions
107 | })
108 |
109 | When("there is a failed condition", func() {
110 | BeforeEach(func() {
111 | o := job.Object()
112 | o.Status.Conditions = []v1.JobCondition{
113 | {
114 | Type: v1.JobFailed,
115 | Message: "Some failure message",
116 | },
117 | }
118 | })
119 |
120 | It("returns status failed", func() {
121 | Expect(s.Failed.State).To(BeTrue())
122 | Expect(s.Failed.Message).To(Equal("Some failure message"))
123 | })
124 | })
125 |
126 | When("the job is not finished", func() {
127 | It("returns the expected status", func() {
128 | Expect(s.Stable.State).To(BeFalse())
129 | Expect(s.Stable.Message).To(Equal("Waiting for jobs to finish"))
130 | })
131 | })
132 | })
133 |
134 | When("it succeeeds", func() {
135 | It("succeeds", func() {
136 | Expect(s.Stable.State).To(BeTrue())
137 | Expect(s.Available.State).To(BeTrue())
138 | })
139 | })
140 | })
141 | })
142 |
--------------------------------------------------------------------------------
/internal/kubernetes/kubernetes_suite_test.go:
--------------------------------------------------------------------------------
1 | package kubernetes_test
2 |
3 | import (
4 | "testing"
5 |
6 | . "github.com/onsi/ginkgo/v2"
7 | . "github.com/onsi/gomega"
8 | )
9 |
10 | func TestKubernetes(t *testing.T) {
11 | RegisterFailHandler(Fail)
12 | RunSpecs(t, "Kubernetes Suite")
13 | }
14 |
--------------------------------------------------------------------------------
/internal/kubernetes/label.go:
--------------------------------------------------------------------------------
1 | package kubernetes
2 |
3 | import (
4 | "strings"
5 |
6 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
7 | )
8 |
9 | const (
10 | // https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/
11 | LabelKubernetesName = `app.kubernetes.io/name`
12 | LabelKubernetesManagedBy = `app.kubernetes.io/managed-by`
13 | LabelSpinnakerMonikerSequence = `moniker.spinnaker.io/sequence`
14 | )
15 |
16 | // AddSpinnakerLabels labels a given unstructured Kubernetes resource
17 | // with Spinnaker defined labels.
18 | func AddSpinnakerLabels(u *unstructured.Unstructured, application string) error {
19 | // Add reserved labels. Do not overwrite the "qpp.kubernetes.io/name" label
20 | // as this could affect label selectors.
21 | //
22 | // https://spinnaker.io/reference/providers/kubernetes-v2/#reserved-labels
23 | // https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/
24 | label(u, LabelKubernetesManagedBy, spinnaker)
25 | labelIfNotExists(u, LabelKubernetesName, application)
26 |
27 | if strings.EqualFold(u.GetKind(), "deployment") ||
28 | strings.EqualFold(u.GetKind(), "replicaset") ||
29 | strings.EqualFold(u.GetKind(), "daemonset") {
30 | err := labelTemplate(u, LabelKubernetesManagedBy, spinnaker)
31 | if err != nil {
32 | return err
33 | }
34 |
35 | err = labelTemplateIfNotExists(u, LabelKubernetesName, application)
36 | if err != nil {
37 | return err
38 | }
39 | }
40 |
41 | return nil
42 | }
43 |
44 | // AddSpinnakerVersionLabels adds the `moniker.spinnaker.io/sequence` label
45 | // to the manifest to identify the version number of that resource.
46 | func AddSpinnakerVersionLabels(u *unstructured.Unstructured, version SpinnakerVersion) error {
47 | label(u, LabelSpinnakerMonikerSequence, version.Short)
48 |
49 | if strings.EqualFold(u.GetKind(), "deployment") ||
50 | strings.EqualFold(u.GetKind(), "replicaset") ||
51 | strings.EqualFold(u.GetKind(), "statefulset") ||
52 | strings.EqualFold(u.GetKind(), "daemonset") {
53 | err := labelTemplate(u, LabelSpinnakerMonikerSequence, version.Short)
54 | if err != nil {
55 | return err
56 | }
57 | }
58 |
59 | return nil
60 | }
61 |
62 | func label(u *unstructured.Unstructured, key, value string) {
63 | labels := u.GetLabels()
64 | if labels == nil {
65 | labels = map[string]string{}
66 | }
67 |
68 | labels[key] = value
69 | u.SetLabels(labels)
70 | }
71 |
72 | func labelTemplate(u *unstructured.Unstructured, key, value string) error {
73 | templateLabels, found, err := unstructured.NestedStringMap(u.Object, "spec", "template", "metadata", "labels")
74 | if err != nil {
75 | return err
76 | }
77 |
78 | if !found {
79 | templateLabels = map[string]string{}
80 | }
81 |
82 | templateLabels[key] = value
83 |
84 | err = unstructured.SetNestedStringMap(u.Object, templateLabels, "spec", "template", "metadata", "labels")
85 | if err != nil {
86 | return err
87 | }
88 |
89 | return nil
90 | }
91 |
92 | func labelIfNotExists(u *unstructured.Unstructured, key, value string) {
93 | labels := u.GetLabels()
94 | if labels == nil {
95 | labels = map[string]string{}
96 | }
97 |
98 | if _, ok := labels[key]; !ok {
99 | labels[key] = value
100 | }
101 |
102 | u.SetLabels(labels)
103 | }
104 |
105 | func labelTemplateIfNotExists(u *unstructured.Unstructured, key, value string) error {
106 | templateLabels, found, err := unstructured.NestedStringMap(u.Object, "spec", "template", "metadata", "labels")
107 | if err != nil {
108 | return err
109 | }
110 |
111 | if !found {
112 | templateLabels = map[string]string{}
113 | }
114 |
115 | if templateLabels[key] != "" {
116 | return nil
117 | }
118 |
119 | templateLabels[key] = value
120 |
121 | err = unstructured.SetNestedStringMap(u.Object, templateLabels, "spec", "template", "metadata", "labels")
122 | if err != nil {
123 | return err
124 | }
125 |
126 | return nil
127 | }
128 |
--------------------------------------------------------------------------------
/internal/kubernetes/label_selector.go:
--------------------------------------------------------------------------------
1 | package kubernetes
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8 | "k8s.io/apimachinery/pkg/labels"
9 | "k8s.io/apimachinery/pkg/selection"
10 | )
11 |
12 | func NewRequirement(op string, key string, values []string) (*labels.Requirement, error) {
13 | switch strings.ToLower(op) {
14 | case "contains":
15 | return labels.NewRequirement(key, selection.In, values)
16 | case "not_contains":
17 | return labels.NewRequirement(key, selection.NotIn, values)
18 | case "equals":
19 | return labels.NewRequirement(key, selection.Equals, values)
20 | case "not_equals":
21 | return labels.NewRequirement(key, selection.NotEquals, values)
22 | case "exists":
23 | return labels.NewRequirement(key, selection.Exists, values)
24 | case "not_exists":
25 | return labels.NewRequirement(key, selection.DoesNotExist, values)
26 | default:
27 | return nil, fmt.Errorf("operator '%v' is not recognized", op)
28 | }
29 | }
30 |
31 | // DefaultLabelSelector returns the label selector
32 | // `app.kubernetes.io/managed-by in (spinnaker,spinnaker-operator)`,
33 | // which allows us to list all resources with a label selector
34 | // managed by Spinnaker or Spinnaker Operator.
35 | func DefaultLabelSelector() string {
36 | labelSelector := metav1.LabelSelector{
37 | MatchExpressions: []metav1.LabelSelectorRequirement{
38 | {
39 | Key: LabelKubernetesManagedBy,
40 | Operator: metav1.LabelSelectorOpIn,
41 | Values: []string{"spinnaker", "spinnaker-operator"},
42 | },
43 | },
44 | }
45 | // Since this is a defined label Selector we can ignore the error.
46 | ls, _ := metav1.LabelSelectorAsSelector(&labelSelector)
47 |
48 | return ls.String()
49 | }
50 |
--------------------------------------------------------------------------------
/internal/kubernetes/manifest/status.go:
--------------------------------------------------------------------------------
1 | package manifest
2 |
3 | var DefaultStatus = Status{
4 | Available: StatusAvailable{
5 | State: true,
6 | },
7 | Failed: StatusFailed{
8 | State: false,
9 | },
10 | Paused: StatusPaused{
11 | State: false,
12 | },
13 | Stable: StatusStable{
14 | State: true,
15 | },
16 | }
17 |
18 | var NoneReported = Status{
19 | Available: StatusAvailable{
20 | State: false,
21 | Message: "No availability reported",
22 | },
23 | Failed: StatusFailed{
24 | State: false,
25 | },
26 | Paused: StatusPaused{
27 | State: false,
28 | },
29 | Stable: StatusStable{
30 | State: false,
31 | Message: "No status reported yet",
32 | },
33 | }
34 |
35 | type Status struct {
36 | Available StatusAvailable `json:"available"`
37 | Failed StatusFailed `json:"failed"`
38 | Paused StatusPaused `json:"paused"`
39 | Stable StatusStable `json:"stable"`
40 | }
41 |
42 | type StatusAvailable struct {
43 | State bool `json:"state"`
44 | Message string `json:"message"`
45 | }
46 |
47 | type StatusFailed struct {
48 | State bool `json:"state"`
49 | Message string `json:"message"`
50 | }
51 |
52 | type StatusPaused struct {
53 | State bool `json:"state"`
54 | Message string `json:"message"`
55 | }
56 |
57 | type StatusStable struct {
58 | State bool `json:"state"`
59 | Message string `json:"message"`
60 | }
61 |
--------------------------------------------------------------------------------
/internal/kubernetes/namespace.go:
--------------------------------------------------------------------------------
1 | package kubernetes
2 |
3 | import (
4 | "strings"
5 |
6 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
7 | )
8 |
9 | // SetNamespaceOnManifest updates the namespace on the given manifest.
10 | //
11 | // If no namespace is set on the manifest and no namespace override is passed
12 | // in then we set the namespace to 'default'.
13 | //
14 | // If namespaceOverride is empty it will NOT override the namespace set
15 | // on the manifest.
16 | //
17 | // We only override the namespace if the manifest is NOT cluster scoped
18 | // (i.e. a ClusterRole) and namespaceOverride is NOT an empty string.
19 | func SetNamespaceOnManifest(u *unstructured.Unstructured, namespaceOverride string) {
20 | if namespaceOverride == "" {
21 | setDefaultNamespaceIfScopedAndNoneSet(u)
22 | } else {
23 | setNamespaceIfScoped(namespaceOverride, u)
24 | }
25 | }
26 |
27 | func setDefaultNamespaceIfScopedAndNoneSet(u *unstructured.Unstructured) {
28 | namespace := u.GetNamespace()
29 | if isNamespaceScoped(u.GetKind()) && namespace == "" {
30 | namespace = "default"
31 | u.SetNamespace(namespace)
32 | }
33 | }
34 |
35 | func setNamespaceIfScoped(namespace string, u *unstructured.Unstructured) {
36 | if isNamespaceScoped(u.GetKind()) {
37 | u.SetNamespace(namespace)
38 | }
39 | }
40 |
41 | // isNamespaceScoped returns true if the kind is namespace-scoped.
42 | //
43 | // Cluster-scoped kinds are:
44 | // - apiService
45 | // - clusterRole
46 | // - clusterRoleBinding
47 | // - customResourceDefinition
48 | // - mutatingWebhookConfiguration
49 | // - namespace
50 | // - persistentVolume
51 | // - podSecurityPolicy
52 | // - storageClass
53 | // - validatingWebhookConfiguration
54 | //
55 | // See https://github.com/spinnaker/clouddriver/blob/58ab154b0ec0d62772201b5b319af349498a4e3f/clouddriver-kubernetes/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/description/manifest/KubernetesKindProperties.java#L31
56 | // for clouddriver OSS namespace-scoped kinds.
57 | func isNamespaceScoped(kind string) bool {
58 | namespaceScoped := true
59 |
60 | for _, value := range clusterScopedKinds {
61 | if strings.EqualFold(value, kind) {
62 | namespaceScoped = false
63 |
64 | break
65 | }
66 | }
67 |
68 | return namespaceScoped
69 | }
70 |
--------------------------------------------------------------------------------
/internal/kubernetes/pod.go:
--------------------------------------------------------------------------------
1 | package kubernetes
2 |
3 | import (
4 | "encoding/json"
5 |
6 | "github.com/homedepot/go-clouddriver/internal/kubernetes/manifest"
7 | v1 "k8s.io/api/core/v1"
8 | )
9 |
10 | func NewPod(m map[string]interface{}) *Pod {
11 | p := &v1.Pod{}
12 | b, _ := json.Marshal(m)
13 | _ = json.Unmarshal(b, &p)
14 |
15 | return &Pod{p: p}
16 | }
17 |
18 | type Pod struct {
19 | p *v1.Pod
20 | }
21 |
22 | func (p *Pod) Object() *v1.Pod {
23 | return p.p
24 | }
25 |
26 | func (p *Pod) Status() manifest.Status {
27 | s := manifest.DefaultStatus
28 |
29 | if p.p.Status.Phase == v1.PodPending ||
30 | p.p.Status.Phase == v1.PodFailed ||
31 | p.p.Status.Phase == v1.PodUnknown {
32 | s.Stable.State = false
33 | s.Stable.Message = "Pod is " + string(p.p.Status.Phase)
34 | s.Available.State = false
35 | s.Available.Message = "Pod is " + string(p.p.Status.Phase)
36 | }
37 |
38 | return s
39 | }
40 |
--------------------------------------------------------------------------------
/internal/kubernetes/pod_test.go:
--------------------------------------------------------------------------------
1 | package kubernetes_test
2 |
3 | import (
4 | . "github.com/onsi/ginkgo/v2"
5 | . "github.com/onsi/gomega"
6 | v1 "k8s.io/api/core/v1"
7 |
8 | . "github.com/homedepot/go-clouddriver/internal/kubernetes"
9 | "github.com/homedepot/go-clouddriver/internal/kubernetes/manifest"
10 | )
11 |
12 | var _ = Describe("Pod", func() {
13 | var (
14 | pod *Pod
15 | )
16 |
17 | BeforeEach(func() {
18 | p := map[string]interface{}{}
19 | pod = NewPod(p)
20 | })
21 |
22 | Describe("#Status", func() {
23 | var s manifest.Status
24 |
25 | JustBeforeEach(func() {
26 | s = pod.Status()
27 | })
28 |
29 | When("pod phase is pending", func() {
30 | BeforeEach(func() {
31 | o := pod.Object()
32 | o.Status.Phase = v1.PodPending
33 | })
34 |
35 | It("returns expected status", func() {
36 | Expect(s.Stable.State).To(BeFalse())
37 | Expect(s.Stable.Message).To(Equal("Pod is Pending"))
38 | Expect(s.Available.State).To(BeFalse())
39 | Expect(s.Available.Message).To(Equal("Pod is Pending"))
40 | })
41 | })
42 |
43 | When("pod phase is failed", func() {
44 | BeforeEach(func() {
45 | o := pod.Object()
46 | o.Status.Phase = v1.PodFailed
47 | })
48 |
49 | It("returns expected status", func() {
50 | Expect(s.Stable.State).To(BeFalse())
51 | Expect(s.Stable.Message).To(Equal("Pod is Failed"))
52 | Expect(s.Available.State).To(BeFalse())
53 | Expect(s.Available.Message).To(Equal("Pod is Failed"))
54 | })
55 | })
56 |
57 | When("pod phase is unknown", func() {
58 | BeforeEach(func() {
59 | o := pod.Object()
60 | o.Status.Phase = v1.PodUnknown
61 | })
62 |
63 | It("returns expected status", func() {
64 | Expect(s.Stable.State).To(BeFalse())
65 | Expect(s.Stable.Message).To(Equal("Pod is Unknown"))
66 | Expect(s.Available.State).To(BeFalse())
67 | Expect(s.Available.Message).To(Equal("Pod is Unknown"))
68 | })
69 | })
70 | })
71 | })
72 |
--------------------------------------------------------------------------------
/internal/kubernetes/provider.go:
--------------------------------------------------------------------------------
1 | package kubernetes
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | "k8s.io/kubectl/pkg/util/slice"
8 | )
9 |
10 | var (
11 | clusterScopedKinds = []string{
12 | "apiService",
13 | "clusterRole",
14 | "clusterRoleBinding",
15 | "customResourceDefinition",
16 | "mutatingWebhookConfiguration",
17 | "namespace",
18 | "persistentVolume",
19 | "podSecurityPolicy",
20 | "storageClass",
21 | "validatingWebhookConfiguration",
22 | }
23 | )
24 |
25 | type Provider struct {
26 | Name string `json:"name" gorm:"primary_key"`
27 | Host string `json:"host"`
28 | CAData string `json:"caData" gorm:"type:text"`
29 | BearerToken string `json:"bearerToken,omitempty" gorm:"size:2048"`
30 | TokenProvider string `json:"tokenProvider,omitempty" gorm:"size:128;not null;default:'google'"`
31 | Namespace *string `json:"namespace,omitempty" gorm:"size:253"`
32 | Namespaces []string `json:"namespaces,omitempty" gorm:"-"`
33 | Permissions ProviderPermissions `json:"permissions" gorm:"-"`
34 | // Providers can hold instances of clients.
35 | Client Client `json:"-" gorm:"-"`
36 | Clientset Clientset `json:"-" gorm:"-"`
37 | }
38 |
39 | type ProviderPermissions struct {
40 | Read []string `json:"read" gorm:"-"`
41 | Write []string `json:"write" gorm:"-"`
42 | }
43 |
44 | func (Provider) TableName() string {
45 | return "kubernetes_providers"
46 | }
47 |
48 | type ProviderNamespaces struct {
49 | // ID string `json:"-" gorm:"primary_key"`
50 | AccountName string `json:"accountName" gorm:"index:account_name_namespace_idx,unique"`
51 | Namespace string `json:"namespace,omitempty" gorm:"index:account_name_namespace_idx,unique"`
52 | }
53 |
54 | func (ProviderNamespaces) TableName() string {
55 | return "kubernetes_providers_namespaces"
56 | }
57 |
58 | // ValidateKindStatus verifies that this provider can access the given kind.
59 | // This begins to support `omitKinds`, but only in the context of namespace-scoped
60 | // providers.
61 | //
62 | // When a provider is limited to namespace, then it cannot access these kinds:
63 | // - apiService
64 | // - clusterRole
65 | // - clusterRoleBinding
66 | // - customResourceDefinition
67 | // - mutatingWebhookConfiguration
68 | // - namespace
69 | // - persistentVolume
70 | // - podSecurityPolicy
71 | // - storageClass
72 | // - validatingWebhookConfiguration
73 | //
74 | // See https://github.com/spinnaker/clouddriver/blob/58ab154b0ec0d62772201b5b319af349498a4e3f/clouddriver-kubernetes/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/description/manifest/KubernetesKindProperties.java#L31
75 | // for clouddriver OSS namespace-scoped kinds.
76 | func (p *Provider) ValidateKindStatus(kind string) error {
77 | if p.Namespace == nil && len(p.Namespaces) == 0 {
78 | return nil
79 | }
80 |
81 | for _, value := range clusterScopedKinds {
82 | if strings.EqualFold(value, kind) {
83 | return fmt.Errorf("namespace-scoped account not allowed to access cluster-scoped kind: '%s'", kind)
84 | }
85 | }
86 |
87 | return nil
88 | }
89 |
90 | // ValidateNamespaceAccess verifies that this provider can access the given namespace
91 | func (p *Provider) ValidateNamespaceAccess(namespace string) error {
92 | namespace = strings.TrimSpace(namespace)
93 |
94 | if namespace == "" {
95 | namespace = "default"
96 | }
97 |
98 | if len(p.Namespaces) > 0 && !slice.ContainsString(p.Namespaces, namespace, nil) {
99 | return fmt.Errorf("namespace-scoped account not allowed to access forbidden namespace: '%s'", namespace)
100 | }
101 |
102 | return nil
103 | }
104 |
105 | // WithClient sets the kubernetes client for this provider.
106 | func (p *Provider) WithClient(client Client) {
107 | p.Client = client
108 | }
109 |
110 | // WithClientset sets the kubernetes clientset for this provider.
111 | func (p *Provider) WithClientset(clientset Clientset) {
112 | p.Clientset = clientset
113 | }
114 |
--------------------------------------------------------------------------------
/internal/kubernetes/provider_test.go:
--------------------------------------------------------------------------------
1 | package kubernetes_test
2 |
3 | import (
4 | . "github.com/homedepot/go-clouddriver/internal/kubernetes"
5 | . "github.com/onsi/ginkgo/v2"
6 | . "github.com/onsi/gomega"
7 | )
8 |
9 | var _ = Describe("Provider", func() {
10 | var (
11 | provider Provider
12 | kind string
13 | namespace string = "test-namespace"
14 | )
15 |
16 | Context("#ValidateKindStatus", func() {
17 | BeforeEach(func() {
18 | provider = Provider{}
19 | kind = "Deployment"
20 | })
21 |
22 | JustBeforeEach(func() {
23 | err = provider.ValidateKindStatus(kind)
24 | })
25 |
26 | When("Provider is namespace-scoped", func() {
27 | BeforeEach(func() {
28 | provider.Namespaces = []string{namespace}
29 | })
30 |
31 | When("kind is not allowed", func() {
32 | BeforeEach(func() {
33 | kind = "Namespace"
34 | })
35 |
36 | It("errors", func() {
37 | Expect(err).ToNot(BeNil())
38 | Expect(err.Error()).To(Equal("namespace-scoped account not allowed to access cluster-scoped kind: 'Namespace'"))
39 | })
40 | })
41 |
42 | When("kind is allowed", func() {
43 | It("succeeds", func() {
44 | Expect(err).To(BeNil())
45 | })
46 | })
47 | })
48 |
49 | When("Provider is cluster-scoped", func() {
50 | When("kind is not allowed", func() {
51 | BeforeEach(func() {
52 | kind = "Namespace"
53 | })
54 |
55 | It("succeeds", func() {
56 | Expect(err).To(BeNil())
57 | })
58 | })
59 |
60 | When("kind is allowed", func() {
61 | It("succeeds", func() {
62 | Expect(err).To(BeNil())
63 | })
64 | })
65 | })
66 | })
67 | })
68 |
--------------------------------------------------------------------------------
/internal/kubernetes/replicaset.go:
--------------------------------------------------------------------------------
1 | package kubernetes
2 |
3 | import (
4 | "encoding/json"
5 |
6 | "github.com/homedepot/go-clouddriver/internal/kubernetes/manifest"
7 | v1 "k8s.io/api/apps/v1"
8 | )
9 |
10 | func NewReplicaSet(m map[string]interface{}) *ReplicaSet {
11 | r := &v1.ReplicaSet{}
12 | b, _ := json.Marshal(m)
13 | _ = json.Unmarshal(b, &r)
14 |
15 | return &ReplicaSet{rs: r}
16 | }
17 |
18 | type ReplicaSet struct {
19 | rs *v1.ReplicaSet
20 | }
21 |
22 | func (rs *ReplicaSet) Object() *v1.ReplicaSet {
23 | return rs.rs
24 | }
25 |
26 | func (rs *ReplicaSet) Status() manifest.Status {
27 | s := manifest.DefaultStatus
28 | r := rs.rs
29 |
30 | desired := int32(0)
31 | fullyLabeled := r.Status.FullyLabeledReplicas
32 | available := r.Status.AvailableReplicas
33 | ready := r.Status.ReadyReplicas
34 |
35 | if r.Spec.Replicas != nil {
36 | desired = *r.Spec.Replicas
37 | }
38 |
39 | if desired > fullyLabeled {
40 | s.Stable.State = false
41 | s.Stable.Message = "Waiting for all replicas to be fully-labeled"
42 |
43 | return s
44 | }
45 |
46 | if desired > ready {
47 | s.Stable.State = false
48 | s.Stable.Message = "Waiting for all replicas to be ready"
49 |
50 | return s
51 | }
52 |
53 | if desired > available {
54 | s.Stable.State = false
55 | s.Stable.Message = "Waiting for all replicas to be available"
56 |
57 | return s
58 | }
59 |
60 | if r.ObjectMeta.Generation != r.Status.ObservedGeneration {
61 | s.Stable.State = false
62 | s.Stable.Message = "Waiting for replicaset spec update to be observed"
63 |
64 | return s
65 | }
66 |
67 | return s
68 | }
69 |
--------------------------------------------------------------------------------
/internal/kubernetes/replicaset_test.go:
--------------------------------------------------------------------------------
1 | package kubernetes_test
2 |
3 | import (
4 | . "github.com/homedepot/go-clouddriver/internal/kubernetes"
5 | "github.com/homedepot/go-clouddriver/internal/kubernetes/manifest"
6 | . "github.com/onsi/ginkgo/v2"
7 | . "github.com/onsi/gomega"
8 | v1 "k8s.io/api/apps/v1"
9 | )
10 |
11 | var _ = Describe("Replicaset", func() {
12 | var (
13 | rs *ReplicaSet
14 | )
15 |
16 | BeforeEach(func() {
17 | r := map[string]interface{}{}
18 | rs = NewReplicaSet(r)
19 | })
20 |
21 | Describe("#Object", func() {
22 | var r *v1.ReplicaSet
23 |
24 | BeforeEach(func() {
25 | r = rs.Object()
26 | })
27 |
28 | When("it succeeds", func() {
29 | It("succeeds", func() {
30 | Expect(r).ToNot(BeNil())
31 | })
32 | })
33 | })
34 |
35 | Describe("#Status", func() {
36 | var s manifest.Status
37 |
38 | BeforeEach(func() {
39 | replicas := int32(4)
40 | rs.Object().Spec.Replicas = &replicas
41 | })
42 |
43 | JustBeforeEach(func() {
44 | s = rs.Status()
45 | })
46 |
47 | When("there are more desired replicas than fully labeled replicas", func() {
48 | It("returns status unstable", func() {
49 | Expect(s.Stable.State).To(BeFalse())
50 | Expect(s.Stable.Message).To(Equal("Waiting for all replicas to be fully-labeled"))
51 | })
52 | })
53 |
54 | When("there are more desired replicas than ready replicas", func() {
55 | BeforeEach(func() {
56 | o := rs.Object()
57 | o.Status.FullyLabeledReplicas = int32(4)
58 | })
59 |
60 | It("returns status unstable", func() {
61 | Expect(s.Stable.State).To(BeFalse())
62 | Expect(s.Stable.Message).To(Equal("Waiting for all replicas to be ready"))
63 | })
64 | })
65 |
66 | When("there are more desired replicas than available", func() {
67 | BeforeEach(func() {
68 | o := rs.Object()
69 | o.Status.FullyLabeledReplicas = int32(4)
70 | o.Status.ReadyReplicas = int32(4)
71 | })
72 |
73 | It("returns status unstable", func() {
74 | Expect(s.Stable.State).To(BeFalse())
75 | Expect(s.Stable.Message).To(Equal("Waiting for all replicas to be available"))
76 | })
77 | })
78 |
79 | When("the generations do not match", func() {
80 | BeforeEach(func() {
81 | o := rs.Object()
82 | o.Status.FullyLabeledReplicas = int32(4)
83 | o.Status.ReadyReplicas = int32(4)
84 | o.Status.AvailableReplicas = int32(4)
85 | o.ObjectMeta.Generation = 100
86 | o.Status.ObservedGeneration = 99
87 | })
88 |
89 | It("returns status unstable", func() {
90 | Expect(s.Stable.State).To(BeFalse())
91 | Expect(s.Stable.Message).To(Equal("Waiting for replicaset spec update to be observed"))
92 | })
93 | })
94 |
95 | When("it succeeds", func() {
96 | BeforeEach(func() {
97 | o := rs.Object()
98 | o.Status.FullyLabeledReplicas = int32(4)
99 | o.Status.ReadyReplicas = int32(4)
100 | o.Status.AvailableReplicas = int32(4)
101 | o.ObjectMeta.Generation = 100
102 | o.Status.ObservedGeneration = 100
103 | })
104 |
105 | It("returns status unstable", func() {
106 | Expect(s.Stable.State).To(BeTrue())
107 | })
108 | })
109 | })
110 | })
111 |
--------------------------------------------------------------------------------
/internal/kubernetes/resource.go:
--------------------------------------------------------------------------------
1 | package kubernetes
2 |
3 | import "time"
4 |
5 | type Resource struct {
6 | AccountName string `json:"accountName" gorm:"index:account_name_kind_name_spinnaker_app_idx,priority:1"`
7 | ID string `json:"id" gorm:"primary_key"`
8 | Timestamp time.Time `json:"timestamp,omitempty" gorm:"type:timestamp;DEFAULT:current_timestamp"`
9 | TaskID string `json:"taskId" gorm:"index:task_id_idx"`
10 | TaskType string `json:"-"`
11 | APIGroup string `json:"apiGroup"`
12 | Name string `json:"name" gorm:"index:account_name_kind_name_spinnaker_app_idx,priority:3"`
13 | ArtifactName string `json:"-"`
14 | Namespace string `json:"namespace"`
15 | Resource string `json:"resource"`
16 | Version string `json:"version"`
17 | Kind string `json:"kind" gorm:"index:account_name_kind_name_spinnaker_app_idx,priority:2;index:kind_idx"`
18 | SpinnakerApp string `json:"spinnakerApp" gorm:"index:account_name_kind_name_spinnaker_app_idx,priority:4"`
19 | Cluster string `json:"-"`
20 | }
21 |
22 | func (Resource) TableName() string {
23 | return "kubernetes_resources"
24 | }
25 |
--------------------------------------------------------------------------------
/internal/kubernetes/statefulset.go:
--------------------------------------------------------------------------------
1 | package kubernetes
2 |
3 | import (
4 | "encoding/json"
5 | "reflect"
6 | "strings"
7 |
8 | "github.com/homedepot/go-clouddriver/internal/kubernetes/manifest"
9 | v1 "k8s.io/api/apps/v1"
10 | )
11 |
12 | func NewStatefulSet(m map[string]interface{}) *StatefulSet {
13 | s := &v1.StatefulSet{}
14 | b, _ := json.Marshal(m)
15 | _ = json.Unmarshal(b, &s)
16 |
17 | return &StatefulSet{ss: s}
18 | }
19 |
20 | type StatefulSet struct {
21 | ss *v1.StatefulSet
22 | }
23 |
24 | func (ss *StatefulSet) Object() *v1.StatefulSet {
25 | return ss.ss
26 | }
27 |
28 | func (ss *StatefulSet) Status() manifest.Status {
29 | s := manifest.DefaultStatus
30 | x := ss.ss
31 |
32 | if strings.EqualFold(string(x.Spec.UpdateStrategy.Type), "ondelete") {
33 | return s
34 | }
35 |
36 | if reflect.DeepEqual(x.Status, v1.StatefulSetStatus{}) {
37 | s = manifest.NoneReported
38 | return s
39 | }
40 |
41 | if x.ObjectMeta.Generation != x.Status.ObservedGeneration {
42 | s.Stable.State = false
43 | s.Stable.Message = "Waiting for status generation to match updated object generation"
44 |
45 | return s
46 | }
47 |
48 | desired := int32(0)
49 | if x.Spec.Replicas != nil {
50 | desired = *x.Spec.Replicas
51 | }
52 |
53 | existing := x.Status.Replicas
54 | if desired > existing {
55 | s.Stable.State = false
56 | s.Stable.Message = "Waiting for at least the desired replica count to be met"
57 |
58 | return s
59 | }
60 |
61 | ready := x.Status.ReadyReplicas
62 | if desired > ready {
63 | s.Stable.State = false
64 | s.Stable.Message = "Waiting for all updated replicas to be ready"
65 |
66 | return s
67 | }
68 |
69 | updType := string(x.Spec.UpdateStrategy.Type)
70 | rollUpd := x.Spec.UpdateStrategy.RollingUpdate
71 | updated := x.Status.UpdatedReplicas
72 |
73 | if strings.EqualFold(updType, "rollingupdate") && rollUpd != nil {
74 | partition := rollUpd.Partition
75 | if partition != nil && (updated < (existing - *partition)) {
76 | s.Stable.State = false
77 | s.Stable.Message = "Waiting for partitioned rollout to finish"
78 |
79 | return s
80 | }
81 |
82 | s.Stable.State = true
83 | s.Stable.Message = "Partitioned roll out complete"
84 |
85 | return s
86 | }
87 |
88 | current := x.Status.CurrentReplicas
89 | if desired > current {
90 | s.Stable.State = false
91 | s.Stable.Message = "Waiting for all updated replicas to be scheduled"
92 |
93 | return s
94 | }
95 |
96 | updateRev := x.Status.UpdateRevision
97 | currentRev := x.Status.CurrentRevision
98 |
99 | if currentRev != updateRev {
100 | s.Stable.State = false
101 | s.Stable.Message = "Waiting for the updated revision to match the current revision"
102 |
103 | return s
104 | }
105 |
106 | return s
107 | }
108 |
--------------------------------------------------------------------------------
/internal/kubernetes/statefulset_test.go:
--------------------------------------------------------------------------------
1 | package kubernetes_test
2 |
3 | import (
4 | . "github.com/homedepot/go-clouddriver/internal/kubernetes"
5 | "github.com/homedepot/go-clouddriver/internal/kubernetes/manifest"
6 | . "github.com/onsi/ginkgo/v2"
7 | . "github.com/onsi/gomega"
8 | v1 "k8s.io/api/apps/v1"
9 | )
10 |
11 | var _ = Describe("Statefulset", func() {
12 | var (
13 | ss *StatefulSet
14 | )
15 |
16 | BeforeEach(func() {
17 | s := map[string]interface{}{}
18 | ss = NewStatefulSet(s)
19 | })
20 |
21 | Describe("#Object", func() {
22 | var s *v1.StatefulSet
23 |
24 | BeforeEach(func() {
25 | s = ss.Object()
26 | })
27 |
28 | When("it succeeds", func() {
29 | It("succeeds", func() {
30 | Expect(s).ToNot(BeNil())
31 | })
32 | })
33 | })
34 |
35 | Describe("#Status", func() {
36 | var s manifest.Status
37 |
38 | BeforeEach(func() {
39 | replicas := int32(4)
40 | o := ss.Object()
41 | o.Spec.Replicas = &replicas
42 | o.Status.Replicas = replicas
43 | o.Status.ReadyReplicas = replicas
44 | o.Status.UpdatedReplicas = replicas
45 | o.Status.CurrentReplicas = replicas
46 | })
47 |
48 | JustBeforeEach(func() {
49 | s = ss.Status()
50 | })
51 |
52 | When("the update strategy type is OnDelete", func() {
53 | BeforeEach(func() {
54 | o := ss.Object()
55 | o.Spec.UpdateStrategy.Type = "OnDelete"
56 | })
57 |
58 | It("returns the expected status", func() {
59 | Expect(s).To(Equal(manifest.DefaultStatus))
60 | })
61 | })
62 |
63 | When("there is no reported status", func() {
64 | BeforeEach(func() {
65 | o := ss.Object()
66 | o.Status = v1.StatefulSetStatus{}
67 | })
68 |
69 | It("returns the expected status", func() {
70 | Expect(s).To(Equal(manifest.NoneReported))
71 | })
72 | })
73 |
74 | When("the generation does not match", func() {
75 | BeforeEach(func() {
76 | o := ss.Object()
77 | o.ObjectMeta.Generation = int64(99)
78 | o.Status.ObservedGeneration = int64(100)
79 | })
80 |
81 | It("returns the expected status", func() {
82 | Expect(s.Stable.State).To(BeFalse())
83 | Expect(s.Stable.Message).To(Equal("Waiting for status generation to match updated object generation"))
84 | })
85 | })
86 |
87 | When("there are more desired replicas than existing replicas", func() {
88 | BeforeEach(func() {
89 | o := ss.Object()
90 | o.Status.Replicas = 3
91 | })
92 |
93 | It("returns the expected status", func() {
94 | Expect(s.Stable.State).To(BeFalse())
95 | Expect(s.Stable.Message).To(Equal("Waiting for at least the desired replica count to be met"))
96 | })
97 | })
98 |
99 | When("there are more desired replicas than ready replicas", func() {
100 | BeforeEach(func() {
101 | o := ss.Object()
102 | o.Status.ReadyReplicas = int32(3)
103 | })
104 |
105 | It("returns the expected status", func() {
106 | Expect(s.Stable.State).To(BeFalse())
107 | Expect(s.Stable.Message).To(Equal("Waiting for all updated replicas to be ready"))
108 | })
109 | })
110 |
111 | Context("when the update type is a rolling update", func() {
112 | BeforeEach(func() {
113 | o := ss.Object()
114 | o.Spec.UpdateStrategy.Type = "RollingUpdate"
115 | rollingUpdate := v1.RollingUpdateStatefulSetStrategy{}
116 | o.Spec.UpdateStrategy.RollingUpdate = &rollingUpdate
117 | })
118 |
119 | When("the partitioned rollout has not finished", func() {
120 | BeforeEach(func() {
121 | partition := int32(2)
122 | o := ss.Object()
123 | o.Status.UpdatedReplicas = 1
124 | o.Status.CurrentReplicas = 3
125 | o.Spec.UpdateStrategy.RollingUpdate.Partition = &partition
126 | })
127 |
128 | It("returns the expected status", func() {
129 | Expect(s.Stable.State).To(BeFalse())
130 | Expect(s.Stable.Message).To(Equal("Waiting for partitioned rollout to finish"))
131 | })
132 | })
133 |
134 | When("the partitioned rollout has finished", func() {
135 | BeforeEach(func() {
136 | partition := int32(2)
137 | o := ss.Object()
138 | o.Status.UpdatedReplicas = 2
139 | o.Spec.UpdateStrategy.RollingUpdate.Partition = &partition
140 | })
141 |
142 | It("returns the expected status", func() {
143 | Expect(s.Stable.State).To(BeTrue())
144 | Expect(s.Stable.Message).To(Equal("Partitioned roll out complete"))
145 | })
146 | })
147 | })
148 |
149 | When("the desired replicas is more than the current replicas", func() {
150 | BeforeEach(func() {
151 | o := ss.Object()
152 | o.Status.CurrentReplicas = 2
153 | })
154 |
155 | It("returns the expected status", func() {
156 | Expect(s.Stable.State).To(BeFalse())
157 | Expect(s.Stable.Message).To(Equal("Waiting for all updated replicas to be scheduled"))
158 | })
159 | })
160 |
161 | When("the current revision is not equal to the update revision", func() {
162 | BeforeEach(func() {
163 | o := ss.Object()
164 | o.Status.UpdateRevision = "100"
165 | o.Status.CurrentRevision = "99"
166 | })
167 |
168 | It("returns the expected status", func() {
169 | Expect(s.Stable.State).To(BeFalse())
170 | Expect(s.Stable.Message).To(Equal("Waiting for the updated revision to match the current revision"))
171 | })
172 | })
173 |
174 | When("it succeeds", func() {
175 | It("succeeds", func() {
176 | Expect(s).To(Equal(manifest.DefaultStatus))
177 | })
178 | })
179 | })
180 | })
181 |
--------------------------------------------------------------------------------
/internal/kubernetes/status.go:
--------------------------------------------------------------------------------
1 | package kubernetes
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/homedepot/go-clouddriver/internal/kubernetes/manifest"
7 | )
8 |
9 | // Status definitions of kinds can be found at
10 | // https://github.com/spinnaker/clouddriver/tree/master/clouddriver-kubernetes/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/op/handler
11 | func GetStatus(kind string, m map[string]interface{}) manifest.Status {
12 | var status manifest.Status
13 |
14 | switch strings.ToLower(kind) {
15 | case "daemonset":
16 | status = NewDaemonSet(m).Status()
17 | case "deployment":
18 | status = NewDeployment(m).Status()
19 | case "horizontalpodautoscaler":
20 | status = NewHorizontalPodAutoscaler(m).Status()
21 | case "job":
22 | status = NewJob(m).Status()
23 | case "pod":
24 | status = NewPod(m).Status()
25 | case "replicaset":
26 | status = NewReplicaSet(m).Status()
27 | case "statefulset":
28 | status = NewStatefulSet(m).Status()
29 | default:
30 | status = NewCustomKind(kind, m).Status()
31 | }
32 |
33 | return status
34 | }
35 |
--------------------------------------------------------------------------------
/internal/kubernetes/strategy.go:
--------------------------------------------------------------------------------
1 | package kubernetes
2 |
3 | import (
4 | "strconv"
5 | "strings"
6 |
7 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
8 | )
9 |
10 | const (
11 | AnnotationSpinnakerMaxVersionHistory = "strategy.spinnaker.io/max-version-history"
12 | AnnotationSpinnakerRecreate = "strategy.spinnaker.io/recreate"
13 | AnnotationSpinnakerReplaced = "strategy.spinnaker.io/replace"
14 | AnnotationSpinnakerUseSourceCapacity = "strategy.spinnaker.io/use-source-capacity"
15 | )
16 |
17 | // MaxVersionHistory returns the value as an int of the annotation
18 | // `strategy.spinnaker.io/max-version-history` of the given Kubernetes
19 | // unstructured resource, or 0 if annotation is not present.
20 | //
21 | // See https://spinnaker.io/docs/reference/providers/kubernetes-v2/#strategy for more info.
22 | func MaxVersionHistory(u unstructured.Unstructured) (maxVersionHistory int, err error) {
23 | maxVersionHistory = 0
24 |
25 | annotations := u.GetAnnotations()
26 | if annotations != nil {
27 | if value, ok := annotations[AnnotationSpinnakerMaxVersionHistory]; ok {
28 | maxVersionHistory, err = strconv.Atoi(value)
29 | if err != nil {
30 | return
31 | }
32 | }
33 | }
34 |
35 | return
36 | }
37 |
38 | // Recreate returns true if the given Kubernetes unstructured resource
39 | // has the annotation `strategy.spinnaker.io/recreate` set to "true".
40 | //
41 | // See https://spinnaker.io/docs/reference/providers/kubernetes-v2/#strategy for more info.
42 | func Recreate(u unstructured.Unstructured) bool {
43 | annotations := u.GetAnnotations()
44 | if annotations != nil {
45 | if value, ok := annotations[AnnotationSpinnakerRecreate]; ok {
46 | return value == "true"
47 | }
48 | }
49 |
50 | return false
51 | }
52 |
53 | // Replace returns true if the given Kubernetes unstructured resource
54 | // has the annotation `strategy.spinnaker.io/replace` set to "true".
55 | //
56 | // See https://spinnaker.io/docs/reference/providers/kubernetes-v2/#strategy for more info.
57 | func Replace(u unstructured.Unstructured) bool {
58 | annotations := u.GetAnnotations()
59 | if annotations != nil {
60 | if value, ok := annotations[AnnotationSpinnakerReplaced]; ok {
61 | return value == "true"
62 | }
63 | }
64 |
65 | return false
66 | }
67 |
68 | // UseSourceCapacity returns true is a given Kubernetes unstructured resource
69 | // has the annotation `strategy.spinnaker.io/use-source-capacity` set to "true"
70 | // and it is of kind Deployment, ReplicaSet, or StatefulSet.
71 | //
72 | // See https://spinnaker.io/docs/reference/providers/kubernetes-v2/#strategy for more info.
73 | func UseSourceCapacity(u unstructured.Unstructured) bool {
74 | switch strings.ToLower(u.GetKind()) {
75 | case "deployment", "replicaset", "statefulset":
76 | annotations := u.GetAnnotations()
77 | if annotations != nil {
78 | if value, ok := annotations[AnnotationSpinnakerUseSourceCapacity]; ok {
79 | return value == "true"
80 | }
81 | }
82 | }
83 |
84 | return false
85 | }
86 |
--------------------------------------------------------------------------------
/internal/kubernetes/traffic.go:
--------------------------------------------------------------------------------
1 | package kubernetes
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 |
7 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
8 | )
9 |
10 | const (
11 | AnnotationSpinnakerTrafficLoadBalancers = "traffic.spinnaker.io/load-balancers"
12 | )
13 |
14 | // LoadBalancers returns a slice of load balancers from the annotation
15 | // `traffic.spinnaker.io/load-balancers`. It errors if this annotation
16 | // is not a string slice format like '["service my-service", "service my-service2"]'.
17 | //
18 | // See https://spinnaker.io/docs/reference/providers/kubernetes-v2/#traffic for more info.
19 | func LoadBalancers(u unstructured.Unstructured) ([]string, error) {
20 | var lbs []string
21 |
22 | annotations := u.GetAnnotations()
23 | if annotations != nil {
24 | if value, ok := annotations[AnnotationSpinnakerTrafficLoadBalancers]; ok {
25 | err := json.Unmarshal([]byte(value), &lbs)
26 | if err != nil {
27 | return nil,
28 | fmt.Errorf("error unmarshaling annotation 'traffic.spinnaker.io/load-balancers' "+
29 | "for resource (kind: %s, name: %s, namespace: %s) into string slice: %v",
30 | u.GetKind(),
31 | u.GetName(),
32 | u.GetNamespace(),
33 | err)
34 | }
35 | }
36 | }
37 |
38 | return lbs, nil
39 | }
40 |
--------------------------------------------------------------------------------
/internal/kubernetes/traffic_test.go:
--------------------------------------------------------------------------------
1 | package kubernetes_test
2 |
3 | import (
4 | . "github.com/homedepot/go-clouddriver/internal/kubernetes"
5 | . "github.com/onsi/ginkgo/v2"
6 | . "github.com/onsi/gomega"
7 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
8 | )
9 |
10 | var _ = Describe("Traffic", func() {
11 | var (
12 | err error
13 | fakeResource unstructured.Unstructured
14 | lbs []string
15 | )
16 |
17 | Context("#LoadBalancers", func() {
18 | JustBeforeEach(func() {
19 | lbs, err = LoadBalancers(fakeResource)
20 | })
21 |
22 | When("annotation is missing", func() {
23 | BeforeEach(func() {
24 | fakeResource = unstructured.Unstructured{
25 | Object: map[string]interface{}{
26 | "kind": "ReplicaSet",
27 | },
28 | }
29 | })
30 |
31 | It("returns no load balancers", func() {
32 | Expect(err).To(BeNil())
33 | Expect(lbs).To(HaveLen(0))
34 | })
35 | })
36 |
37 | When("the annotation is not an array", func() {
38 | BeforeEach(func() {
39 | fakeResource = unstructured.Unstructured{
40 | Object: map[string]interface{}{
41 | "kind": "ReplicaSet",
42 | "metadata": map[string]interface{}{
43 | "name": "test-name",
44 | "namespace": "test-namespace",
45 | "annotations": map[string]interface{}{
46 | "traffic.spinnaker.io/load-balancers": "string",
47 | },
48 | },
49 | },
50 | }
51 | })
52 |
53 | It("errors", func() {
54 | Expect(err).ToNot(BeNil())
55 | Expect(err.Error()).To(Equal("error unmarshaling annotation 'traffic.spinnaker.io/load-balancers' for resource (kind: ReplicaSet, name: test-name, namespace: test-namespace) into string slice: invalid character 's' looking for beginning of value"))
56 | })
57 | })
58 |
59 | When("it succeeds", func() {
60 | BeforeEach(func() {
61 | fakeResource = unstructured.Unstructured{
62 | Object: map[string]interface{}{
63 | "kind": "ReplicaSet",
64 | "metadata": map[string]interface{}{
65 | "name": "test-name",
66 | "namespace": "test-namespace",
67 | "annotations": map[string]interface{}{
68 | "traffic.spinnaker.io/load-balancers": "[\"service test-lb-service\",\"service test-lb-service2\"]",
69 | },
70 | },
71 | },
72 | }
73 | })
74 |
75 | It("succeeds", func() {
76 | Expect(err).To(BeNil())
77 | Expect(lbs).To(HaveLen(2))
78 | Expect(lbs[0]).To(Equal("service test-lb-service"))
79 | Expect(lbs[1]).To(Equal("service test-lb-service2"))
80 | })
81 | })
82 | })
83 | })
84 |
--------------------------------------------------------------------------------
/internal/kubernetes/unstructured.go:
--------------------------------------------------------------------------------
1 | package kubernetes
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 |
7 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
8 | "k8s.io/apimachinery/pkg/runtime"
9 | "k8s.io/cli-runtime/pkg/resource"
10 | )
11 |
12 | // ToUnstructured converts a map[string]interface{} to a kubernetes unstructured.Unstructured
13 | // object. An unstructured's "Object" field is technically just a map[string]interface{},
14 | // so we could do the following:
15 | //
16 | // return unstructured.Unstructured{ Object: manifest }
17 | //
18 | // and not have this function return an error, but we miss out on some validation that
19 | // happens during `kubectl`, such as when you attempt to apply a bad manifest, like something
20 | // without "kind" specified. Example:
21 | //
22 | // $ k apply -f bad.yaml --validate=false
23 | // error: unable to decode "bad.yaml": Object 'Kind' is missing in '{}'
24 | //
25 | // If we decide to not cycle through the encoding/decoding process, these errors
26 | // will be deferred to when the manifest gets applied, and will fail with some error like
27 | //
28 | // error applying manifest (kind: , apiVersion: v1, name: bad-gke): no matches for kind "" in version "apps/v1"
29 | //
30 | // For now, we are not deferring the error.
31 | func ToUnstructured(manifest map[string]interface{}) (unstructured.Unstructured, error) {
32 | b, err := json.Marshal(manifest)
33 | if err != nil {
34 | return unstructured.Unstructured{}, err
35 | }
36 |
37 | obj, _, err := unstructured.UnstructuredJSONScheme.Decode(b, nil, nil)
38 | if err != nil {
39 | return unstructured.Unstructured{}, err
40 | }
41 | // Convert the runtime.Object to unstructured.Unstructured.
42 | m, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
43 | if err != nil {
44 | return unstructured.Unstructured{}, err
45 | }
46 |
47 | u := unstructured.Unstructured{
48 | Object: m,
49 | }
50 |
51 | // Attempt to get annotations as map[string]string
52 | // If no errors then nothing else needs to be done
53 | if _, _, err := unstructured.NestedStringMap(u.Object, "metadata", "annotations"); err == nil {
54 | return u, nil
55 | }
56 |
57 | // Attempt to get annotations as map[string]interface{}
58 | annotationsMap, exists, err := unstructured.NestedMap(u.Object, "metadata", "annotations")
59 | if err != nil || !exists {
60 | return u, err
61 | }
62 |
63 | // If annotations exist in manifest and are map[string]interface, convert to map[string]string
64 | annotations := make(map[string]string, len(annotationsMap))
65 |
66 | for k, v := range annotationsMap {
67 | annotations[k] = fmt.Sprintf("%v", v)
68 | }
69 |
70 | u.SetAnnotations(annotations)
71 |
72 | return u, nil
73 | }
74 |
75 | func SetDefaultNamespaceIfScopedAndNoneSet(u *unstructured.Unstructured, helper *resource.Helper) {
76 | namespace := u.GetNamespace()
77 | if helper.NamespaceScoped && namespace == "" {
78 | namespace = "default"
79 | u.SetNamespace(namespace)
80 | }
81 | }
82 |
83 | func SetNamespaceIfScoped(namespace string, u *unstructured.Unstructured, helper *resource.Helper) {
84 | if helper.NamespaceScoped {
85 | u.SetNamespace(namespace)
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/internal/kubernetes/unstructured_test.go:
--------------------------------------------------------------------------------
1 | package kubernetes_test
2 |
3 | import (
4 | . "github.com/onsi/ginkgo/v2"
5 | . "github.com/onsi/gomega"
6 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
7 | "k8s.io/cli-runtime/pkg/resource"
8 |
9 | . "github.com/homedepot/go-clouddriver/internal/kubernetes"
10 | )
11 |
12 | var _ = Describe("Unstructured", func() {
13 | var (
14 | m map[string]interface{}
15 | err error
16 | )
17 |
18 | Describe("#ToUnstructured", func() {
19 | var u unstructured.Unstructured
20 |
21 | BeforeEach(func() {
22 | m = map[string]interface{}{
23 | "kind": "Namespace",
24 | "apiVersion": "v1",
25 | }
26 | })
27 |
28 | JustBeforeEach(func() {
29 | u, err = ToUnstructured(m)
30 | })
31 |
32 | When("object kind is missing", func() {
33 | BeforeEach(func() {
34 | m = map[string]interface{}{}
35 | })
36 |
37 | It("returns an error", func() {
38 | Expect(err).ToNot(BeNil())
39 | Expect(err.Error()).To(Equal("Object 'Kind' is missing in '{}'"))
40 | })
41 | })
42 |
43 | When("it succeeds", func() {
44 | It("succeeds", func() {
45 | Expect(err).To(BeNil())
46 | Expect(u).ToNot(BeNil())
47 | })
48 | })
49 | })
50 |
51 | Describe("#SetDefaultNamespaceIfScopedAndNoneSet", func() {
52 | var (
53 | u unstructured.Unstructured
54 | helper *resource.Helper
55 | )
56 |
57 | BeforeEach(func() {
58 | m = map[string]interface{}{
59 | "kind": "Pod",
60 | "apiVersion": "v1",
61 | "metadata": map[string]interface{}{},
62 | }
63 | helper = &resource.Helper{
64 | NamespaceScoped: false,
65 | }
66 | u, err = ToUnstructured(m)
67 | Expect(err).To(BeNil())
68 | })
69 |
70 | JustBeforeEach(func() {
71 | SetDefaultNamespaceIfScopedAndNoneSet(&u, helper)
72 | })
73 |
74 | When("it is scoped", func() {
75 | BeforeEach(func() {
76 | helper.NamespaceScoped = true
77 | })
78 |
79 | It("sets the default namespace", func() {
80 | n := u.GetNamespace()
81 | Expect(n).To(Equal("default"))
82 | })
83 | })
84 |
85 | When("it is not scoped", func() {
86 | BeforeEach(func() {
87 | helper.NamespaceScoped = false
88 | })
89 |
90 | It("does not set the namespace", func() {
91 | n := u.GetNamespace()
92 | Expect(n).To(BeEmpty())
93 | })
94 | })
95 | })
96 | })
97 |
--------------------------------------------------------------------------------
/internal/kubernetes/version.go:
--------------------------------------------------------------------------------
1 | package kubernetes
2 |
3 | import (
4 | "fmt"
5 | "regexp"
6 | "sort"
7 | "strconv"
8 | "strings"
9 |
10 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
11 | )
12 |
13 | const (
14 | // https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/
15 | AnnotationSpinnakerArtifactVersion = `artifact.spinnaker.io/version`
16 | AnnotationSpinnakerMonikerSequence = `moniker.spinnaker.io/sequence`
17 | // Maximum latest version before cycling back to v000.
18 | maxLatestVersion = 999
19 | )
20 |
21 | var (
22 | // Regular expresion to match trailing '-v###'
23 | matchSpinnakerVersionRegexp = regexp.MustCompile("-v[0-9]{3}[0-9]*$")
24 | )
25 |
26 | type SpinnakerVersion struct {
27 | Long string
28 | Short string
29 | }
30 |
31 | // GetCurrentVersion returns the latest "Spinnaker version" from an unstructured
32 | // list of Kubernetes resources.
33 | func GetCurrentVersion(ul *unstructured.UnstructuredList, kind, name string) string {
34 | currentVersion := "-1"
35 | cluster := ""
36 |
37 | if ul == nil || len(ul.Items) == 0 {
38 | return currentVersion
39 | }
40 |
41 | // Filter out all unassociated objects based on the
42 | // moniker.spinnaker.io/cluster annotation.
43 | cluster = kind + " " + name
44 | results := FilterOnAnnotation(ul.Items, AnnotationSpinnakerMonikerCluster, cluster)
45 | // Filter out empty moniker.spinnaker.io/sequence labels
46 | results = FilterOnLabelExists(results, LabelSpinnakerMonikerSequence)
47 | if len(results) == 0 {
48 | return currentVersion
49 | }
50 |
51 | // For now, we sort on creation timestamp to grab the manifest.
52 | sort.Slice(results, func(i, j int) bool {
53 | return results[i].GetCreationTimestamp().String() > results[j].GetCreationTimestamp().String()
54 | })
55 |
56 | annotations := results[0].GetAnnotations()
57 | currentVersion = annotations[AnnotationSpinnakerMonikerSequence]
58 |
59 | return currentVersion
60 | }
61 |
62 | // IsVersioned returns true is a given Kubernetes unstructured resource
63 | // is "versioned". A resource is version if its annotation
64 | // `strategy.spinnaker.io/versioned` is set to "true" or if it is of kind
65 | // Pod, ReplicaSet, ConfigMap, or Secret.
66 | //
67 | // See https://spinnaker.io/reference/providers/kubernetes-v2/#workloads for more info.
68 | func IsVersioned(u unstructured.Unstructured) bool {
69 | annotations := u.GetAnnotations()
70 | if annotations != nil {
71 | if _, ok := annotations[AnnotationSpinnakerStrategyVersioned]; ok {
72 | return annotations[AnnotationSpinnakerStrategyVersioned] == "true"
73 | }
74 | }
75 |
76 | kind := strings.ToLower(u.GetKind())
77 | if strings.EqualFold(kind, "pod") ||
78 | strings.EqualFold(kind, "replicaSet") ||
79 | strings.EqualFold(kind, "ConfigMap") ||
80 | strings.EqualFold(kind, "Secret") {
81 | return true
82 | }
83 |
84 | return false
85 | }
86 |
87 | func IncrementVersion(currentVersion string) SpinnakerVersion {
88 | currentVersionInt, _ := strconv.Atoi(currentVersion)
89 | latestVersionInt := currentVersionInt + 1
90 |
91 | if latestVersionInt > maxLatestVersion {
92 | latestVersionInt = 0
93 | }
94 |
95 | latestVersionShortFormat := strconv.Itoa(latestVersionInt)
96 | latestVersionLongFormat := ""
97 | latestVersionLongFormat = fmt.Sprintf("v%03d", latestVersionInt)
98 |
99 | return SpinnakerVersion{
100 | Short: latestVersionShortFormat,
101 | Long: latestVersionLongFormat,
102 | }
103 | }
104 |
105 | // NameWithVersion removes the Spinnaker version (trailing '-v###)
106 | // from the name.
107 | func NameWithoutVersion(name string) string {
108 | return matchSpinnakerVersionRegexp.ReplaceAllString(name, "")
109 | }
110 |
--------------------------------------------------------------------------------
/internal/map.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | )
7 |
8 | // DeleteNilVaules returns the given map with all keys
9 | // with nil values removed.
10 | func DeleteNilValues(m map[string]interface{}) map[string]interface{} {
11 | newMap := make(map[string]interface{})
12 |
13 | for k, v := range m {
14 | if v != nil {
15 | if isMap(v) {
16 | newMap[k] = DeleteNilValues(v.(map[string]interface{}))
17 | } else {
18 | newMap[k] = v
19 | }
20 | }
21 | }
22 |
23 | return newMap
24 | }
25 |
26 | // isMap returns true if the argument is of type map
27 | //
28 | // see https://stackoverflow.com/questions/20759803/how-to-check-variable-type-is-map-in-go-language
29 | func isMap(x interface{}) bool {
30 | t := fmt.Sprintf("%T", x)
31 | return strings.HasPrefix(t, "map[")
32 | }
33 |
--------------------------------------------------------------------------------
/internal/map_test.go:
--------------------------------------------------------------------------------
1 | package internal_test
2 |
3 | import (
4 | . "github.com/homedepot/go-clouddriver/internal"
5 | . "github.com/onsi/ginkgo/v2"
6 | . "github.com/onsi/gomega"
7 | )
8 |
9 | var _ = Describe("Map", func() {
10 |
11 | var m, actual map[string]interface{}
12 |
13 | Describe("#DeleteNilValues", func() {
14 |
15 | JustBeforeEach(func() {
16 | actual = DeleteNilValues(m)
17 | })
18 |
19 | When("the map is empty", func() {
20 | BeforeEach(func() {
21 | m = map[string]interface{}{}
22 | })
23 |
24 | It("returns an empty map", func() {
25 | Expect(actual).To(Equal(m))
26 | })
27 | })
28 |
29 | When("the map has no nil-valued keys", func() {
30 | BeforeEach(func() {
31 | m = map[string]interface{}{
32 | "a": 1,
33 | "b": true,
34 | "c": "three",
35 | }
36 | })
37 |
38 | It("returns the map unchanged", func() {
39 | Expect(actual).To(Equal(m))
40 | })
41 | })
42 |
43 | When("the map has nil-valued keys", func() {
44 | BeforeEach(func() {
45 | m = map[string]interface{}{
46 | "a": 1,
47 | "b": nil,
48 | "c": "three",
49 | }
50 | })
51 |
52 | It("succeeds", func() {
53 | expected := map[string]interface{}{
54 | "a": 1,
55 | "c": "three",
56 | }
57 | Expect(actual).To(Equal(expected))
58 | })
59 | })
60 |
61 | When("the map has nested nil-valued keys", func() {
62 | BeforeEach(func() {
63 | m = map[string]interface{}{
64 | "a": 1,
65 | "b": "nil",
66 | "c": "three",
67 | "d": map[string]interface{}{
68 | "e": nil,
69 | "f": "f",
70 | "g": map[string]interface{}{
71 | "h": nil,
72 | },
73 | },
74 | }
75 | })
76 |
77 | It("succeeds", func() {
78 | expected := map[string]interface{}{
79 | "a": 1,
80 | "b": "nil",
81 | "c": "three",
82 | "d": map[string]interface{}{
83 | "f": "f",
84 | "g": map[string]interface{}{},
85 | },
86 | }
87 | Expect(actual).To(Equal(expected))
88 | })
89 | })
90 | })
91 | })
92 |
--------------------------------------------------------------------------------
/internal/middleware/cachecontrol.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/gin-gonic/gin"
7 | )
8 |
9 | // CacheControl sets the 'Cache-Control' header to the defined
10 | // max-age.
11 | func CacheControl(maxAge int) gin.HandlerFunc {
12 | return func(c *gin.Context) {
13 | c.Writer.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", maxAge))
14 | c.Next()
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/internal/middleware/cachecontrol_test.go:
--------------------------------------------------------------------------------
1 | package middleware_test
2 |
3 | import (
4 | "net/http"
5 | "net/http/httptest"
6 |
7 | "github.com/gin-gonic/gin"
8 | . "github.com/homedepot/go-clouddriver/internal/middleware"
9 | . "github.com/onsi/ginkgo/v2"
10 | . "github.com/onsi/gomega"
11 | )
12 |
13 | var _ = Describe("CacheControl", func() {
14 | var (
15 | c *gin.Context
16 | r *http.Request
17 | err error
18 | )
19 |
20 | BeforeEach(func() {
21 | gin.SetMode(gin.ReleaseMode)
22 | c, _ = gin.CreateTestContext(httptest.NewRecorder())
23 | r, err = http.NewRequest(http.MethodGet, "", nil)
24 | Expect(err).To(BeNil())
25 | c.Request = r
26 | })
27 |
28 | JustBeforeEach(func() {
29 | mw := CacheControl(30)
30 | mw(c)
31 | })
32 |
33 | It("sets the Cache-Control header", func() {
34 | Expect(c.Writer.Header().Get("Cache-Control")).To(Equal("public, max-age=30"))
35 | })
36 | })
37 |
--------------------------------------------------------------------------------
/internal/middleware/controller.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import "github.com/homedepot/go-clouddriver/internal"
4 |
5 | type Controller struct {
6 | *internal.Controller
7 | }
8 |
--------------------------------------------------------------------------------
/internal/middleware/error.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/gin-gonic/gin"
7 | clouddriver "github.com/homedepot/go-clouddriver/pkg"
8 | )
9 |
10 | func HandleError() gin.HandlerFunc {
11 | return func(c *gin.Context) {
12 | c.Next() // execute all the handlers
13 |
14 | // If an error occurred during handling the request, write the error as a JSON response.
15 | err := c.Errors.ByType(gin.ErrorTypePublic).Last()
16 | if err != nil {
17 | statusCode := c.Writer.Status()
18 | text := http.StatusText(statusCode)
19 |
20 | if statusCode >= http.StatusInternalServerError {
21 | meta := clouddriver.Meta(err)
22 | clouddriver.Log(err, meta)
23 | text += " (error ID: " + meta.GUID + ")"
24 | }
25 |
26 | ce := clouddriver.NewError(
27 | text,
28 | err.Error(),
29 | statusCode,
30 | )
31 |
32 | c.JSON(c.Writer.Status(), ce)
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/internal/middleware/log.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/base64"
7 | "encoding/json"
8 | "fmt"
9 | "io"
10 | "log"
11 | "time"
12 |
13 | "github.com/fatih/color"
14 | "github.com/gin-gonic/gin"
15 | clouddriver "github.com/homedepot/go-clouddriver/pkg"
16 | )
17 |
18 | var (
19 | bold = color.New(color.FgWhite, color.Bold).SprintFunc()
20 | )
21 |
22 | // A verbose request/response logger.
23 | func LogRequest() gin.HandlerFunc {
24 | return func(c *gin.Context) {
25 | var (
26 | err error
27 | buf bytes.Buffer
28 | )
29 |
30 | clone := c.Request.Clone(context.TODO())
31 |
32 | _, err = buf.ReadFrom(c.Request.Body)
33 | c.Request.Body = io.NopCloser(&buf)
34 | clone.Body = io.NopCloser(bytes.NewReader(buf.Bytes()))
35 |
36 | if err != nil {
37 | clouddriver.Log(err)
38 | } else {
39 | b, _ := io.ReadAll(clone.Body)
40 | buffer := &bytes.Buffer{}
41 |
42 | buffer.WriteString(bold("REQUEST: ["+time.Now().In(time.UTC).Format(time.RFC3339)) + bold("]"))
43 | buffer.WriteByte('\n')
44 | buffer.WriteString(fmt.Sprintf("%s %s %s", clone.Method, clone.URL, clone.Proto))
45 | buffer.WriteByte('\n')
46 | buffer.WriteString(fmt.Sprintf("Host: %s", clone.Host))
47 | buffer.WriteByte('\n')
48 | buffer.WriteString(fmt.Sprintf("Accept: %s", clone.Header.Get("Accept")))
49 | buffer.WriteByte('\n')
50 | buffer.WriteString(fmt.Sprintf("User-Agent: %s", clone.Header.Get("User-Agent")))
51 | buffer.WriteByte('\n')
52 | buffer.WriteString(fmt.Sprintf("Headers: %s", clone.Header))
53 | buffer.WriteByte('\n')
54 |
55 | if len(b) > 0 {
56 | j, err := json.MarshalIndent(b, "", " ")
57 | if err != nil {
58 | log.Fatal("[MIDDLEWARE] failed to generate json", err)
59 | } else {
60 | body, _ := base64.StdEncoding.DecodeString(string(j[1 : len(j)-1]))
61 | buffer.Write(body)
62 | buffer.WriteByte('\n')
63 | }
64 | }
65 |
66 | fmt.Println(buffer.String())
67 | }
68 |
69 | c.Next() // execute all the handlers
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/internal/middleware/middleware_suite_test.go:
--------------------------------------------------------------------------------
1 | package middleware_test
2 |
3 | import (
4 | "testing"
5 |
6 | . "github.com/onsi/ginkgo/v2"
7 | . "github.com/onsi/gomega"
8 | )
9 |
10 | func TestMiddleware(t *testing.T) {
11 | RegisterFailHandler(Fail)
12 | RunSpecs(t, "Middleware Suite")
13 | }
14 |
--------------------------------------------------------------------------------
/internal/middleware/task.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "github.com/google/uuid"
6 | clouddriver "github.com/homedepot/go-clouddriver/pkg"
7 | )
8 |
9 | // TaskID attaches a unique GUID to the context.
10 | func TaskID() gin.HandlerFunc {
11 | return func(c *gin.Context) {
12 | c.Set(clouddriver.TaskIDKey, uuid.New().String())
13 | c.Next()
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/internal/middleware/vary.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/gin-gonic/gin"
7 | )
8 |
9 | // Vary sets the 'Vary' header to the defined headers.
10 | func Vary(headers ...string) gin.HandlerFunc {
11 | return func(c *gin.Context) {
12 | c.Writer.Header().Set("Vary", strings.Join(headers, ", "))
13 | c.Next()
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/internal/middleware/vary_test.go:
--------------------------------------------------------------------------------
1 | package middleware_test
2 |
3 | import (
4 | "net/http"
5 | "net/http/httptest"
6 |
7 | "github.com/gin-gonic/gin"
8 | . "github.com/homedepot/go-clouddriver/internal/middleware"
9 | . "github.com/onsi/ginkgo/v2"
10 | . "github.com/onsi/gomega"
11 | )
12 |
13 | var _ = Describe("Vary", func() {
14 | var (
15 | c *gin.Context
16 | r *http.Request
17 | err error
18 | )
19 |
20 | BeforeEach(func() {
21 | gin.SetMode(gin.ReleaseMode)
22 | c, _ = gin.CreateTestContext(httptest.NewRecorder())
23 | r, err = http.NewRequest(http.MethodGet, "", nil)
24 | Expect(err).To(BeNil())
25 | c.Request = r
26 | })
27 |
28 | JustBeforeEach(func() {
29 | mw := Vary("header1", "header2")
30 | mw(c)
31 | })
32 |
33 | It("sets the Vary header", func() {
34 | Expect(c.Writer.Header().Get("Vary")).To(Equal("header1, header2"))
35 | })
36 | })
37 |
--------------------------------------------------------------------------------
/internal/sql/sql_suite_test.go:
--------------------------------------------------------------------------------
1 | package sql_test
2 |
3 | import (
4 | . "github.com/onsi/ginkgo/v2"
5 | . "github.com/onsi/gomega"
6 |
7 | "testing"
8 | )
9 |
10 | func TestSql(t *testing.T) {
11 | RegisterFailHandler(Fail)
12 | RunSpecs(t, "Sql Suite")
13 | }
14 |
--------------------------------------------------------------------------------
/internal/time.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | func CurrentTimeUTC() time.Time {
8 | utc := time.Now().UTC().Format("2006-01-02T15:04:05.999Z")
9 | t, _ := time.Parse("2006-01-02T15:04:05.999Z", utc)
10 |
11 | return t
12 | }
13 |
--------------------------------------------------------------------------------
/internal/time_test.go:
--------------------------------------------------------------------------------
1 | package internal_test
2 |
3 | import (
4 | "time"
5 |
6 | . "github.com/homedepot/go-clouddriver/internal"
7 | . "github.com/onsi/ginkgo/v2"
8 | . "github.com/onsi/gomega"
9 | )
10 |
11 | var _ = Describe("Time", func() {
12 | It("formats and sets the time to UTC", func() {
13 | now := time.Now().UTC().Format("2006-01-02T15:04:05.999Z")
14 | t, _ := time.Parse("2006-01-02T15:04:05.999Z", now)
15 | utc := CurrentTimeUTC()
16 |
17 | Expect(utc).To(BeTemporally("~", t))
18 | })
19 | })
20 |
--------------------------------------------------------------------------------
/pkg/artifact.go:
--------------------------------------------------------------------------------
1 | package clouddriver
2 |
3 | import "github.com/homedepot/go-clouddriver/internal/artifact"
4 |
5 | type Artifact struct {
6 | CustomKind bool `json:"customKind"`
7 | Location string `json:"location,omitempty"`
8 | Metadata ArtifactMetadata `json:"metadata"`
9 | Name string `json:"name"`
10 | Reference string `json:"reference"`
11 | Type artifact.Type `json:"type"`
12 | Version string `json:"version,omitempty"`
13 | }
14 |
15 | type ArtifactMetadata struct {
16 | Account string `json:"account,omitempty"`
17 | }
18 |
--------------------------------------------------------------------------------
/pkg/credentials.go:
--------------------------------------------------------------------------------
1 | package clouddriver
2 |
3 | type Credentials struct {
4 | AccountType string `json:"accountType"`
5 | CacheThreads int `json:"cacheThreads"`
6 | ChallengeDestructiveActions bool `json:"challengeDestructiveActions"`
7 | CloudProvider string `json:"cloudProvider"`
8 | DockerRegistries []interface{} `json:"dockerRegistries"`
9 | Enabled bool `json:"enabled"`
10 | Environment string `json:"environment"`
11 | Name string `json:"name"`
12 | Namespaces []string `json:"namespaces"`
13 | Permissions struct {
14 | READ []string `json:"READ"`
15 | WRITE []string `json:"WRITE"`
16 | } `json:"permissions"`
17 | PrimaryAccount bool `json:"primaryAccount"`
18 | ProviderVersion string `json:"providerVersion"`
19 | RequiredGroupMembership []interface{} `json:"requiredGroupMembership"`
20 | Skin string `json:"skin"`
21 | SpinnakerKindMap map[string]string `json:"spinnakerKindMap"`
22 | Type string `json:"type"`
23 | }
24 |
--------------------------------------------------------------------------------
/pkg/error.go:
--------------------------------------------------------------------------------
1 | package clouddriver
2 |
3 | import (
4 | "runtime"
5 | "time"
6 |
7 | "github.com/gin-gonic/gin"
8 | "github.com/google/uuid"
9 | )
10 |
11 | // This represents a clouddriver error.
12 | type ErrorResponse struct {
13 | Error string `json:"error"`
14 | Message string `json:"message"`
15 | Status int `json:"status"`
16 | Timestamp int64 `json:"timestamp"`
17 | }
18 |
19 | type ErrorMeta struct {
20 | FuncName string
21 | FileName string
22 | GUID string
23 | LineNum int
24 | }
25 |
26 | // Example.
27 | //
28 | // {
29 | // "error":"Forbidden",
30 | // "message":"Access denied to account spin-cluster-account - required authorization: READ",
31 | // "status":403,
32 | // "timestamp":1597608027851
33 | // }
34 | func NewError(err, message string, status int) ErrorResponse {
35 | return ErrorResponse{
36 | Error: err,
37 | Message: message,
38 | Status: status,
39 | Timestamp: time.Now().UnixNano() / 1000000,
40 | }
41 | }
42 |
43 | // Error attaches a given Go error to a gin context and sets its type to public.
44 | func Error(c *gin.Context, status int, err error) {
45 | pc, fn, ln, _ := runtime.Caller(1)
46 | m := ErrorMeta{
47 | FuncName: runtime.FuncForPC(pc).Name(),
48 | FileName: fn,
49 | GUID: uuid.New().String(),
50 | LineNum: ln,
51 | }
52 |
53 | c.Status(status)
54 | _ = c.Error(err).SetType(gin.ErrorTypePublic).SetMeta(m)
55 | }
56 |
57 | func Meta(msg *gin.Error) ErrorMeta {
58 | return msg.Meta.(ErrorMeta)
59 | }
60 |
--------------------------------------------------------------------------------
/pkg/logger.go:
--------------------------------------------------------------------------------
1 | package clouddriver
2 |
3 | import (
4 | "log"
5 | "runtime"
6 | "time"
7 |
8 | "github.com/google/uuid"
9 | )
10 |
11 | const (
12 | white = "\033[90;47m"
13 | reset = "\033[0m"
14 | )
15 |
16 | // Log logs a given error. It checks if meta was passed in. If
17 | // no meta was passed in, it defines the meta as the function and
18 | // line number of what called the Log func.
19 | //
20 | // See https://stackoverflow.com/questions/24809287/how-do-you-get-a-golang-program-to-print-the-line-number-of-the-error-it-just-ca
21 | func Log(err error, meta ...ErrorMeta) {
22 | if len(meta) == 0 {
23 | // notice that we're using 1, so it will actually log the where
24 | // the error happened, 0 = this function, we don't want that.
25 | pc, fn, ln, _ := runtime.Caller(1)
26 | m := ErrorMeta{
27 | FuncName: runtime.FuncForPC(pc).Name(),
28 | FileName: fn,
29 | GUID: uuid.New().String(),
30 | LineNum: ln,
31 | }
32 | meta = append(meta, m)
33 | }
34 |
35 | m := meta[0]
36 |
37 | log.SetFlags(0)
38 | log.Printf("[CLOUDDRIVER] %v |%s %s %s| %s | %s | %s:%d | %v\n",
39 | time.Now().In(time.UTC).Format("2006/01/02 - 15:04:05"),
40 | white, "LOG", reset,
41 | m.GUID,
42 | m.FuncName,
43 | m.FileName,
44 | m.LineNum,
45 | err,
46 | )
47 | }
48 |
--------------------------------------------------------------------------------
/pkg/permissions.go:
--------------------------------------------------------------------------------
1 | package clouddriver
2 |
3 | type Permissions struct {
4 | READ []string `json:"READ"`
5 | WRITE []string `json:"WRITE"`
6 | }
7 |
8 | type ReadPermission struct {
9 | ID string `json:"-" gorm:"primary_key"`
10 | AccountName string `json:"accountName" gorm:"index:account_name_idx"`
11 | ReadGroup string `json:"readGroup"`
12 | }
13 |
14 | func (ReadPermission) TableName() string {
15 | return "provider_read_permissions"
16 | }
17 |
18 | type WritePermission struct {
19 | ID string `json:"-" gorm:"primary_key"`
20 | AccountName string `json:"accountName" gorm:"index:account_name_idx"`
21 | WriteGroup string `json:"writeGroup"`
22 | }
23 |
24 | func (WritePermission) TableName() string {
25 | return "provider_write_permissions"
26 | }
27 |
--------------------------------------------------------------------------------
/pkg/task.go:
--------------------------------------------------------------------------------
1 | package clouddriver
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | )
6 |
7 | const (
8 | TaskIDKey = `TaskID`
9 | TaskTypeCleanup = `cleanup`
10 | TaskTypeDelete = `delete`
11 | TaskTypeNoOp = `noop`
12 | )
13 |
14 | func NewDefaultTask(id string) Task {
15 | return Task{
16 | ID: id,
17 | ResultObjects: []TaskResultObject{},
18 | Status: TaskStatus{
19 | Complete: true,
20 | Completed: true,
21 | Failed: false,
22 | Phase: "ORCHESTRATION",
23 | Retryable: false,
24 | Status: "Orchestration completed.",
25 | },
26 | }
27 | }
28 |
29 | func TaskIDFromContext(c *gin.Context) string {
30 | return c.MustGet(TaskIDKey).(string)
31 | }
32 |
33 | type Task struct {
34 | ID string `json:"id"`
35 | // SagaIds []interface{} `json:"sagaIds"`
36 | // History []struct {
37 | // Phase string `json:"phase"`
38 | // Status string `json:"status"`
39 | // } `json:"history"`
40 | // OwnerIDClouddriverSQL string `json:"ownerId$clouddriver_sql"`
41 | // RequestIDClouddriverSQL string `json:"requestId$clouddriver_sql"`
42 | // Retryable bool `json:"retryable"`
43 | // StartTimeMsClouddriverSQL int64 `json:"startTimeMs$clouddriver_sql"`
44 | ResultObjects []TaskResultObject `json:"resultObjects"`
45 | Status TaskStatus `json:"status"`
46 | }
47 |
48 | type TaskStatus struct {
49 | Complete bool `json:"complete"`
50 | Completed bool `json:"completed"`
51 | Failed bool `json:"failed"`
52 | Phase string `json:"phase"`
53 | Retryable bool `json:"retryable"`
54 | Status string `json:"status"`
55 | }
56 |
57 | type TaskResultObject struct {
58 | BoundArtifacts []Artifact `json:"boundArtifacts"`
59 | CreatedArtifacts []Artifact `json:"createdArtifacts"`
60 | DeployedNamesByLocation map[string][]string `json:"deployedNamesByLocation"`
61 | ManifestNamesByNamespace map[string][]string `json:"manifestNamesByNamespace"`
62 | ManifestNamesByNamespaceToRefresh map[string][]string `json:"manifestNamesByNamespaceToRefresh"`
63 | Manifests []map[string]interface{} `json:"manifests"`
64 | }
65 |
--------------------------------------------------------------------------------