├── .gitignore ├── templates ├── service-settings │ └── gate.yml ├── profiles │ ├── front50-local.yml │ ├── settings-local.js │ └── gate-local.yml ├── profiles-auth │ ├── settings-local.js │ └── gate-local.yml ├── addons │ ├── demo │ │ ├── demok8s │ │ │ ├── pipelines │ │ │ │ ├── last-modified.json.tmpl │ │ │ │ └── PIPELINE_UUID │ │ │ │ │ └── pipeline-metadata.json.tmpl │ │ │ └── applications │ │ │ │ ├── last-modified.json.tmpl │ │ │ │ └── demok8s │ │ │ │ ├── application-permissions.json.tmpl │ │ │ │ └── application-metadata.json.tmpl │ │ └── democanary │ │ │ ├── pipelines │ │ │ ├── last-modified.json.tmpl │ │ │ └── PIPELINE_UUID │ │ │ │ └── pipeline-metadata.json.tmpl │ │ │ ├── applications │ │ │ ├── last-modified.json.tmpl │ │ │ └── democanary │ │ │ │ ├── application-permissions.json.tmpl │ │ │ │ └── application-metadata.json.tmpl │ │ │ └── canary_config │ │ │ └── Latency.json.tmpl │ └── prometheus │ │ ├── prometheus-service.yaml │ │ ├── prometheus-ingress-noauth.yaml │ │ └── prometheus-ingress.yaml ├── manifests │ ├── namespace.yml │ ├── spinnaker-default-clusteradmin-clusterrolebinding.yml │ ├── spinnaker-ingress.yml │ ├── halyard.yml │ ├── mariadb.yml │ └── minio.yml ├── config-armory ├── config └── archive │ ├── minio-without-pvc.yml │ ├── mariadb-without-pvc.yml │ └── halyard-with-pvc.yml ├── bin └── build.sh ├── pipelines ├── armory-app.yaml ├── namespaces.yaml ├── aws-app.yaml ├── kustomization.yaml ├── armory-github.yaml ├── armory-jenkins.yaml ├── armory-slack.yaml ├── armory-basic-k8s-pipe.yaml └── bootstrap.yaml ├── scripts ├── README.md ├── uninstall-k3s.sh ├── utils │ ├── scale_local.sh │ ├── remove_auth.sh │ ├── undo_expose_local.sh │ ├── expose_local.sh │ ├── switch_to_oss.sh │ └── external_service_setup.sh ├── addons │ ├── spinnaker_enable_canary.sh │ ├── setup_demo.sh │ ├── install_prometheus_osx.sh │ ├── install_prometheus.sh │ └── setup_demo_canary.sh ├── refresh_endpoint.sh ├── regenerate_password.sh ├── osx_install.sh ├── no_auth_install.sh ├── install.sh └── functions.sh ├── .github └── workflows │ └── main.yml ├── guides ├── set-up-slack-notifications.md ├── setup-ldap.md ├── add-kubernetes-cluster.md ├── first-pipeline-jenkins.md └── setup-dev-environment.md ├── LICENSE └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | workspace 2 | build 3 | .DS_Store -------------------------------------------------------------------------------- /templates/service-settings/gate.yml: -------------------------------------------------------------------------------- 1 | healthEndpoint: /api/v1/health -------------------------------------------------------------------------------- /templates/profiles/front50-local.yml: -------------------------------------------------------------------------------- 1 | spinnaker.s3.versioning: false 2 | -------------------------------------------------------------------------------- /templates/profiles-auth/settings-local.js: -------------------------------------------------------------------------------- 1 | window.spinnakerSettings.authEnabled = true; 2 | -------------------------------------------------------------------------------- /templates/addons/demo/demok8s/pipelines/last-modified.json.tmpl: -------------------------------------------------------------------------------- 1 | {"lastModified":__TIMESTAMP__} 2 | -------------------------------------------------------------------------------- /templates/profiles/settings-local.js: -------------------------------------------------------------------------------- 1 | window.spinnakerSettings.feature.kustomizeEnabled = true; 2 | -------------------------------------------------------------------------------- /templates/addons/demo/democanary/pipelines/last-modified.json.tmpl: -------------------------------------------------------------------------------- 1 | {"lastModified":__TIMESTAMP__} 2 | -------------------------------------------------------------------------------- /templates/addons/demo/demok8s/applications/last-modified.json.tmpl: -------------------------------------------------------------------------------- 1 | {"lastModified":__TIMESTAMP__} 2 | -------------------------------------------------------------------------------- /templates/addons/demo/democanary/applications/last-modified.json.tmpl: -------------------------------------------------------------------------------- 1 | {"lastModified":__TIMESTAMP__} 2 | -------------------------------------------------------------------------------- /templates/manifests/namespace.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | name: NAMESPACE 6 | -------------------------------------------------------------------------------- /templates/config-armory: -------------------------------------------------------------------------------- 1 | armory: 2 | diagnostics: 3 | enabled: true 4 | uuid: cafed00d 5 | logging: 6 | enabled: true 7 | -------------------------------------------------------------------------------- /templates/profiles-auth/gate-local.yml: -------------------------------------------------------------------------------- 1 | security: 2 | basicform: 3 | enabled: true 4 | user: 5 | name: admin 6 | password: SPINNAKER_PASSWORD 7 | -------------------------------------------------------------------------------- /bin/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | 4 | mkdir -p build/minnaker 5 | cp -rpv templates scripts operator build/minnaker 6 | cd build && tar -czvf minnaker.tgz minnaker -------------------------------------------------------------------------------- /templates/addons/demo/demok8s/applications/demok8s/application-permissions.json.tmpl: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demok8s", 3 | "lastModified": __TIMESTAMP__, 4 | "lastModifiedBy": "demo", 5 | "permissions": {} 6 | } -------------------------------------------------------------------------------- /templates/addons/demo/democanary/applications/democanary/application-permissions.json.tmpl: -------------------------------------------------------------------------------- 1 | { 2 | "name": "democanary", 3 | "lastModified": __TIMESTAMP__, 4 | "lastModifiedBy": "demo", 5 | "permissions": {} 6 | } -------------------------------------------------------------------------------- /pipelines/armory-app.yaml: -------------------------------------------------------------------------------- 1 | #file: application.yaml 2 | apiVersion: pacrd.armory.spinnaker.io/v1alpha1 3 | kind: Application 4 | metadata: 5 | name: armory-samples 6 | spec: 7 | email: test@armory.io 8 | description: Description 9 | -------------------------------------------------------------------------------- /templates/profiles/gate-local.yml: -------------------------------------------------------------------------------- 1 | server: 2 | servlet: 3 | context-path: /api/v1 4 | tomcat: 5 | protocolHeader: X-Forwarded-Proto 6 | remoteIpHeader: X-Forwarded-For 7 | internalProxies: .* 8 | httpsServerPort: X-Forwarded-Port 9 | -------------------------------------------------------------------------------- /pipelines/namespaces.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: dev 5 | --- 6 | apiVersion: v1 7 | kind: Namespace 8 | metadata: 9 | name: stage 10 | --- 11 | apiVersion: v1 12 | kind: Namespace 13 | metadata: 14 | name: prod -------------------------------------------------------------------------------- /templates/addons/prometheus/prometheus-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: prometheus 5 | spec: 6 | ports: 7 | - name: web 8 | port: 9090 9 | protocol: TCP 10 | targetPort: web 11 | selector: 12 | app: prometheus 13 | type: ClusterIP -------------------------------------------------------------------------------- /pipelines/aws-app.yaml: -------------------------------------------------------------------------------- 1 | #file: application.yaml 2 | apiVersion: pacrd.armory.spinnaker.io/v1alpha1 3 | kind: Application 4 | metadata: 5 | name: armory-aws 6 | spec: 7 | email: test@armory.io 8 | description: Pre-built AWS armory app with pipelines for all targets (EC2, ECS, Fargate, EKS, Lambda) 9 | -------------------------------------------------------------------------------- /templates/addons/prometheus/prometheus-ingress-noauth.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: prom-ingress 5 | spec: 6 | rules: 7 | - http: 8 | paths: 9 | - backend: 10 | serviceName: prometheus-operated 11 | servicePort: 9090 12 | path: /prometheus -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | # Scripts 2 | 3 | This directory holds the scripts that make up the core of Minnaker. 4 | 5 | We use `yml` instead of `yaml` for consistency (all service-settings and profiles require `yml`) 6 | 7 | ## TODOs 8 | 9 | * Fix osx_install.sh to use kustomize 10 | * Fix no_auth_install.sh to use kustomize 11 | * Fix addons/ and utils/ scripts 12 | -------------------------------------------------------------------------------- /templates/manifests/spinnaker-default-clusteradmin-clusterrolebinding.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1beta1 3 | kind: ClusterRoleBinding 4 | metadata: 5 | name: NAMESPACE-default-admin 6 | roleRef: 7 | apiGroup: rbac.authorization.k8s.io 8 | kind: ClusterRole 9 | name: cluster-admin 10 | subjects: 11 | - kind: ServiceAccount 12 | name: default 13 | namespace: NAMESPACE 14 | -------------------------------------------------------------------------------- /pipelines/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # file: kustomization.yaml 2 | resources: 3 | - armory-app.yaml 4 | - armory-basic-k8s-pipe.yaml 5 | - armory-jenkins.yaml 6 | - armory-github.yaml 7 | - armory-slack.yaml 8 | - bootstrap.yaml 9 | - namespaces.yaml 10 | 11 | #patchesStrategicMerge: 12 | # - patch.yaml 13 | #namespace: spinnaker # Note: you should change this value if you are _not_ deploying into the `spinnaker` namespace. 14 | -------------------------------------------------------------------------------- /templates/addons/prometheus/prometheus-ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: prom-ingress 5 | annotations: 6 | traefik.ingress.kubernetes.io/auth-type: basic 7 | traefik.ingress.kubernetes.io/auth-secret: prometheus-auth 8 | spec: 9 | rules: 10 | - http: 11 | paths: 12 | - backend: 13 | serviceName: prometheus-operated 14 | servicePort: 9090 15 | path: /prometheus -------------------------------------------------------------------------------- /templates/addons/demo/demok8s/applications/demok8s/application-metadata.json.tmpl: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DEMOK8S", 3 | "description": null, 4 | "email": "demo@armory.io", 5 | "updateTs": "__TIMESTAMP__", 6 | "createTs": "__TIMESTAMP__", 7 | "lastModifiedBy": "demo", 8 | "cloudProviders": "kubernetes", 9 | "trafficGuards": [], 10 | "instancePort": 80, 11 | "user": "demo", 12 | "dataSources": { 13 | "disabled": [], 14 | "enabled": [] 15 | } 16 | } -------------------------------------------------------------------------------- /templates/manifests/spinnaker-ingress.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: networking.k8s.io/v1beta1 3 | kind: Ingress 4 | metadata: 5 | labels: 6 | app: spin 7 | name: spin-ingress 8 | namespace: NAMESPACE 9 | spec: 10 | rules: 11 | - 12 | http: 13 | paths: 14 | - backend: 15 | serviceName: spin-deck 16 | servicePort: 9000 17 | path: / 18 | - backend: 19 | serviceName: spin-gate 20 | servicePort: 8084 21 | path: /api/v1 22 | -------------------------------------------------------------------------------- /templates/addons/demo/democanary/applications/democanary/application-metadata.json.tmpl: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DEMOCANARY", 3 | "description": null, 4 | "email": "demo@armory.io", 5 | "updateTs": "__TIMESTAMP__", 6 | "createTs": "__TIMESTAMP__", 7 | "lastModifiedBy": "demo", 8 | "cloudProviders": "kubernetes", 9 | "trafficGuards": [], 10 | "instancePort": 80, 11 | "user": "demo", 12 | "dataSources": { 13 | "disabled": [], 14 | "enabled": [ 15 | "canaryConfigs" 16 | ] 17 | } 18 | } -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This action creates a new patch release on push to master. 2 | # The behavior can be modified based on commit message or PR label. 3 | # See https://github.com/rymndhng/release-on-push-action#readme for more info. 4 | 5 | name: Publish Patch Release 6 | 7 | on: 8 | push: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | release-on-push: 14 | runs-on: ubuntu-latest 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | steps: 18 | - uses: rymndhng/release-on-push-action@v0.16.0 19 | with: 20 | bump_version_scheme: patch 21 | -------------------------------------------------------------------------------- /pipelines/armory-github.yaml: -------------------------------------------------------------------------------- 1 | # file: deploy-nginx.yaml 2 | apiVersion: pacrd.armory.spinnaker.io/v1alpha1 3 | kind: Pipeline 4 | metadata: 5 | name: configure-github-integration 6 | spec: 7 | description: Click "Start Manual Execution" to the Right for Instructions 8 | application: &app-name armory-samples 9 | stages: 10 | - type: manualJudgment 11 | properties: 12 | name: Configure Github Integration 13 | refId: "1" 14 | failPipeline: true 15 | instructions: "Click Here -> Configure Github
Watch Video and connect Github
" 16 | -------------------------------------------------------------------------------- /pipelines/armory-jenkins.yaml: -------------------------------------------------------------------------------- 1 | # file: deploy-nginx.yaml 2 | apiVersion: pacrd.armory.spinnaker.io/v1alpha1 3 | kind: Pipeline 4 | metadata: 5 | name: configure-jenkins-integration 6 | spec: 7 | description: Click "Start Manual Execution" to the Right for Instructions 8 | application: &app-name armory-samples 9 | stages: 10 | - type: manualJudgment 11 | properties: 12 | name: Configure Jenkins Integration 13 | refId: "1" 14 | failPipeline: true 15 | instructions: "Click Here -> Configure JenkinsWatch Video and Configure Jenkins
" 16 | -------------------------------------------------------------------------------- /pipelines/armory-slack.yaml: -------------------------------------------------------------------------------- 1 | # file: deploy-nginx.yaml 2 | apiVersion: pacrd.armory.spinnaker.io/v1alpha1 3 | kind: Pipeline 4 | metadata: 5 | name: configure-slack-integration 6 | spec: 7 | description: Click "Start Manual Execution" to the Right for Instructions 8 | application: &app-name armory-samples 9 | stages: 10 | - type: manualJudgment 11 | properties: 12 | name: Configure Slack Integration 13 | refId: "1" 14 | failPipeline: true 15 | instructions: "Click Here -> Configure SlackWatch Video and Configure Slack
" 16 | 17 | -------------------------------------------------------------------------------- /scripts/uninstall-k3s.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ################################################################################ 4 | # Copyright 2021 Armory, Inc. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | ################################################################################ 18 | 19 | echo "Uninstalling K3s" 20 | # REMOVE K3S 21 | /usr/local/bin/k3s-uninstall.sh 22 | 23 | -------------------------------------------------------------------------------- /scripts/utils/scale_local.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | NAMESPACE=spinnaker 3 | NAMESPACE_INGRESS=ingress-nginx 4 | 5 | scale() { 6 | kubectl get deployments -n ${NAMESPACE} | awk '{ if (NR > 1) print $1}' | xargs -L1 kubectl scale deploy --replicas=$1 -n ${NAMESPACE} 7 | kubectl get statefulsets -n ${NAMESPACE} | awk '{ if (NR > 1) print $1}' | xargs -L1 kubectl scale sts --replicas=$1 -n ${NAMESPACE} 8 | kubectl get deployments -n ${NAMESPACE_INGRESS} | awk '{ if (NR > 1) print $1}' | xargs -L1 kubectl scale deploy --replicas=$1 -n ${NAMESPACE_INGRESS} 9 | } 10 | 11 | pause=false 12 | 13 | for arg in "$@" 14 | do 15 | case $arg in 16 | -p|--pause) 17 | pause=true 18 | shift 19 | ;; 20 | -r|--resume|-s|--start) 21 | pause=false 22 | shift 23 | ;; 24 | *) 25 | OTHER_ARGUMENTS+=("$1") 26 | shift # Remove generic argument from processing 27 | ;; 28 | esac 29 | done 30 | 31 | if [ $pause == true ] 32 | then 33 | scale 0 34 | else 35 | scale 1 36 | fi -------------------------------------------------------------------------------- /templates/config: -------------------------------------------------------------------------------- 1 | currentDeployment: default 2 | deploymentConfigurations: 3 | - name: default 4 | version: 2.19.8 5 | providers: 6 | kubernetes: 7 | enabled: true 8 | accounts: 9 | - name: spinnaker 10 | providerVersion: V2 11 | serviceAccount: true 12 | onlySpinnakerManaged: true 13 | primaryAccount: spinnaker 14 | deploymentEnvironment: 15 | size: SMALL 16 | type: Distributed 17 | accountName: spinnaker 18 | location: NAMESPACE 19 | persistentStorage: 20 | persistentStoreType: s3 21 | s3: 22 | bucket: spinnaker 23 | rootFolder: front50 24 | pathStyleAccess: true 25 | endpoint: http://minio.spinnaker:9000 26 | accessKeyId: minio 27 | secretAccessKey: MINIO_PASSWORD 28 | features: 29 | artifacts: true 30 | artifactsRewrite: true 31 | security: 32 | apiSecurity: 33 | ssl: 34 | enabled: false 35 | overrideBaseUrl: https://PUBLIC_ENDPOINT/api/v1 36 | uiSecurity: 37 | ssl: 38 | enabled: false 39 | overrideBaseUrl: https://PUBLIC_ENDPOINT 40 | artifacts: 41 | http: 42 | enabled: true 43 | accounts: [] 44 | telemetry: 45 | enabled: true 46 | -------------------------------------------------------------------------------- /templates/archive/minio-without-pvc.yml: -------------------------------------------------------------------------------- 1 | # This is currently not used 2 | --- 3 | apiVersion: apps/v1 4 | kind: StatefulSet 5 | metadata: 6 | name: minio 7 | namespace: spinnaker 8 | spec: 9 | replicas: 1 10 | serviceName: minio 11 | selector: 12 | matchLabels: 13 | app: minio 14 | template: 15 | metadata: 16 | labels: 17 | app: minio 18 | spec: 19 | containers: 20 | - name: minio 21 | image: minio/minio 22 | args: 23 | - server 24 | - /storage 25 | env: 26 | # MinIO access key and secret key 27 | - name: MINIO_ACCESS_KEY 28 | value: "minio" 29 | - name: MINIO_SECRET_KEY 30 | value: "MINIO_PASSWORD" 31 | ports: 32 | - containerPort: 9000 33 | volumeMounts: 34 | - name: storage 35 | mountPath: "/storage" 36 | volumes: 37 | - name: storage 38 | hostPath: 39 | path: BASE_DIR/minio 40 | type: DirectoryOrCreate 41 | --- 42 | apiVersion: v1 43 | kind: Service 44 | metadata: 45 | name: minio 46 | namespace: spinnaker 47 | spec: 48 | ports: 49 | - port: 9000 50 | targetPort: 9000 51 | protocol: TCP 52 | selector: 53 | app: minio 54 | -------------------------------------------------------------------------------- /templates/archive/mariadb-without-pvc.yml: -------------------------------------------------------------------------------- 1 | # This is currently not used 2 | # Do not use this, permissions are not currently set up correctly 3 | --- 4 | apiVersion: apps/v1 5 | kind: StatefulSet 6 | metadata: 7 | name: mariadb 8 | namespace: spinnaker 9 | spec: 10 | replicas: 1 11 | serviceName: mariadb 12 | selector: 13 | matchLabels: 14 | app: mariadb 15 | template: 16 | metadata: 17 | labels: 18 | app: mariadb 19 | spec: 20 | containers: 21 | - name: mariadb 22 | image: mariadb:10.4.12-bionic 23 | volumeMounts: 24 | - name: mysql 25 | mountPath: "/var/lib/mysql" 26 | env: 27 | # - name: HOME 28 | # value: "/home/spinnaker" 29 | - name: MYSQL_ROOT_PASSWORD 30 | value: "MYSQL_PASSWORD" 31 | ports: 32 | - containerPort: 3306 33 | protocol: TCP 34 | volumes: 35 | - name: mysql 36 | hostPath: 37 | path: BASE_DIR/mysql 38 | type: DirectoryOrCreate 39 | --- 40 | apiVersion: v1 41 | kind: Service 42 | metadata: 43 | name: mariadb 44 | namespace: spinnaker 45 | spec: 46 | ports: 47 | - port: 3306 48 | targetPort: 3306 49 | protocol: TCP 50 | selector: 51 | app: mariadb -------------------------------------------------------------------------------- /templates/addons/demo/democanary/canary_config/Latency.json.tmpl: -------------------------------------------------------------------------------- 1 | { 2 | "createdTimestamp": __TIMESTAMP__, 3 | "updatedTimestamp": __TIMESTAMP__, 4 | "createdTimestampIso": "__ISO_TIMESTAMP__", 5 | "updatedTimestampIso": "__ISO_TIMESTAMP__", 6 | "name": "Latency", 7 | "id": "${CANARY_CONFIG_UUID}", 8 | "description": "Latency Canary Config", 9 | "configVersion": "1", 10 | "applications": [ 11 | "democanary" 12 | ], 13 | "judge": { 14 | "name": "NetflixACAJudge-v1.0", 15 | "judgeConfigurations": {} 16 | }, 17 | "metrics": [ 18 | { 19 | "name": "latency", 20 | "query": { 21 | "type": "prometheus", 22 | "metricName": "custom_dummy_latency", 23 | "labelBindings": [], 24 | "groupByFields": [], 25 | "customInlineTemplate": "", 26 | "customFilterTemplate": "Filter", 27 | "serviceType": "prometheus" 28 | }, 29 | "groups": [ 30 | "Latency" 31 | ], 32 | "analysisConfigurations": { 33 | "canary": { 34 | "direction": "increase" 35 | } 36 | }, 37 | "scopeName": "default" 38 | } 39 | ], 40 | "templates": { 41 | "Filter": "group=\"${scope}\",namespace=\"${location}\"" 42 | }, 43 | "classifier": { 44 | "groupWeights": { 45 | "Latency": 100 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /templates/manifests/halyard.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: StatefulSet 4 | metadata: 5 | name: halyard 6 | namespace: NAMESPACE 7 | spec: 8 | replicas: 1 9 | serviceName: halyard 10 | selector: 11 | matchLabels: 12 | app: halyard 13 | template: 14 | metadata: 15 | labels: 16 | app: halyard 17 | spec: 18 | containers: 19 | - name: halyard 20 | image: HALYARD_IMAGE 21 | volumeMounts: 22 | - name: hal 23 | mountPath: "/home/spinnaker/.hal" 24 | - name: kube 25 | mountPath: "/home/spinnaker/.kube" 26 | env: 27 | - name: HOME 28 | value: "/home/spinnaker" 29 | ports: 30 | - containerPort: 8064 31 | protocol: TCP 32 | readinessProbe: 33 | exec: 34 | command: 35 | - wget 36 | - --no-check-certificate 37 | - --spider 38 | - -q 39 | - http://localhost:8064/health 40 | securityContext: 41 | runAsUser: 1000 42 | runAsGroup: 65535 43 | volumes: 44 | - name: hal 45 | hostPath: 46 | path: BASE_DIR/.hal 47 | type: DirectoryOrCreate 48 | - name: kube 49 | hostPath: 50 | path: BASE_DIR/.kube 51 | type: DirectoryOrCreate 52 | -------------------------------------------------------------------------------- /templates/manifests/mariadb.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: PersistentVolumeClaim 4 | metadata: 5 | name: mariadb-pvc 6 | labels: 7 | app: mariadb 8 | namespace: NAMESPACE 9 | spec: 10 | accessModes: 11 | - ReadWriteOnce 12 | resources: 13 | requests: 14 | storage: 4Gi 15 | --- 16 | apiVersion: apps/v1 17 | kind: StatefulSet 18 | metadata: 19 | name: mariadb 20 | namespace: NAMESPACE 21 | spec: 22 | replicas: 1 23 | serviceName: mariadb 24 | selector: 25 | matchLabels: 26 | app: mariadb 27 | template: 28 | metadata: 29 | labels: 30 | app: mariadb 31 | spec: 32 | containers: 33 | - name: mariadb 34 | image: mariadb:10.4.12-bionic 35 | volumeMounts: 36 | - name: mysql 37 | mountPath: "/var/lib/mysql" 38 | env: 39 | - name: MYSQL_ROOT_PASSWORD 40 | value: "MARIADB_PASSWORD" 41 | ports: 42 | - containerPort: 3306 43 | protocol: TCP 44 | securityContext: 45 | runAsUser: 1000 46 | runAsGroup: 65535 47 | fsGroup: 65535 48 | volumes: 49 | - name: mysql 50 | persistentVolumeClaim: 51 | claimName: mariadb-pvc 52 | --- 53 | apiVersion: v1 54 | kind: Service 55 | metadata: 56 | name: mariadb 57 | namespace: NAMESPACE 58 | spec: 59 | ports: 60 | - port: 3306 61 | targetPort: 3306 62 | protocol: TCP 63 | selector: 64 | app: mariadb 65 | -------------------------------------------------------------------------------- /scripts/utils/remove_auth.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ################################################################################ 4 | # Copyright 2020 Armory, Inc. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | ################################################################################ 18 | 19 | set -e 20 | 21 | # Linux only 22 | 23 | PROJECT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )/../../" >/dev/null 2>&1 && pwd ) 24 | KUBERNETES_CONTEXT=default 25 | NAMESPACE=spinnaker 26 | BASE_DIR=/etc/spinnaker 27 | 28 | mv ${BASE_DIR}/.hal/.secret/spinnaker_password ${BASE_DIR}/.hal/.secret/spinnaker_password_removed 29 | yq d -i ${BASE_DIR}/.hal/default/profiles/gate-local.yml security 30 | sed -i 's|^window.spinnakerSettings.authEnabled|# window.spinnakerSettings.authEnabled|g' ${BASE_DIR}/.hal/default/profiles/settings-local.js 31 | 32 | kubectl --context ${KUBERNETES_CONTEXT} -n ${NAMESPACE} exec -i halyard-0 -- sh -c "hal deploy apply" -------------------------------------------------------------------------------- /scripts/utils/undo_expose_local.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ################################################################################ 4 | # Copyright 2020 Armory, Inc. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | ################################################################################ 18 | 19 | set -e 20 | 21 | # Linux only 22 | 23 | PROJECT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )/../../" >/dev/null 2>&1 && pwd ) 24 | KUBERNETES_CONTEXT=default 25 | NAMESPACE=spinnaker 26 | 27 | BASE_DIR=/etc/spinnaker 28 | 29 | for SVC in front50 igor rosco echo deck orca gate kayenta fiat clouddriver redis; do 30 | touch ${BASE_DIR}/.hal/default/service-settings/${SVC}.yml 31 | yq d -i ${BASE_DIR}/.hal/default/service-settings/${SVC}.yml kubernetes.serviceType 32 | done 33 | 34 | kubectl --context ${KUBERNETES_CONTEXT} --namespace ${NAMESPACE} delete svc -l app=spin 35 | 36 | kubectl --context ${KUBERNETES_CONTEXT} -n ${NAMESPACE} exec -i halyard-0 -- sh -c "hal deploy apply" -------------------------------------------------------------------------------- /scripts/utils/expose_local.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ################################################################################ 4 | # Copyright 2020 Armory, Inc. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | ################################################################################ 18 | 19 | set -e 20 | 21 | # Linux only 22 | 23 | PROJECT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )/../../" >/dev/null 2>&1 && pwd ) 24 | KUBERNETES_CONTEXT=default 25 | NAMESPACE=spinnaker 26 | 27 | BASE_DIR=/etc/spinnaker 28 | 29 | for SVC in front50 igor rosco echo deck orca gate kayenta fiat clouddriver redis; do 30 | touch ${BASE_DIR}/.hal/default/service-settings/${SVC}.yml 31 | yq w -i ${BASE_DIR}/.hal/default/service-settings/${SVC}.yml kubernetes.serviceType LoadBalancer 32 | done 33 | 34 | kubectl --context ${KUBERNETES_CONTEXT} --namespace ${NAMESPACE} delete svc -l app=spin 35 | 36 | kubectl --context ${KUBERNETES_CONTEXT} -n ${NAMESPACE} exec -i halyard-0 -- sh -c "hal deploy apply" 37 | -------------------------------------------------------------------------------- /templates/manifests/minio.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: PersistentVolumeClaim 4 | metadata: 5 | name: minio-pvc 6 | labels: 7 | app: minio 8 | namespace: NAMESPACE 9 | spec: 10 | accessModes: 11 | - ReadWriteOnce 12 | resources: 13 | requests: 14 | storage: 1Gi 15 | --- 16 | apiVersion: apps/v1 17 | kind: StatefulSet 18 | metadata: 19 | name: minio 20 | namespace: NAMESPACE 21 | spec: 22 | replicas: 1 23 | serviceName: minio 24 | selector: 25 | matchLabels: 26 | app: minio 27 | template: 28 | metadata: 29 | labels: 30 | app: minio 31 | spec: 32 | containers: 33 | - name: minio 34 | image: minio/minio 35 | args: 36 | - server 37 | - /storage 38 | env: 39 | # MinIO access key and secret key 40 | - name: MINIO_ACCESS_KEY 41 | value: "minio" 42 | - name: MINIO_SECRET_KEY 43 | value: "MINIO_PASSWORD" 44 | ports: 45 | - containerPort: 9000 46 | volumeMounts: 47 | - name: storage 48 | mountPath: "/storage" 49 | securityContext: 50 | runAsUser: 1000 51 | runAsGroup: 65535 52 | fsGroup: 65535 53 | volumes: 54 | - name: storage 55 | persistentVolumeClaim: 56 | claimName: minio-pvc 57 | --- 58 | apiVersion: v1 59 | kind: Service 60 | metadata: 61 | name: minio 62 | namespace: NAMESPACE 63 | spec: 64 | ports: 65 | - port: 9000 66 | targetPort: 9000 67 | protocol: TCP 68 | selector: 69 | app: minio 70 | -------------------------------------------------------------------------------- /guides/set-up-slack-notifications.md: -------------------------------------------------------------------------------- 1 | # Setting up Notifications to Slack 2 | 3 | Spinnaker supports notifications to Slack (and other places) on these six events: 4 | 5 | * Pipeline start 6 | * Pipeline completion 7 | * Pipeline failure 8 | * Stage start 9 | * Stage completion 10 | * Stage failure 11 | 12 | ## Create a Slack bot user (and get the Slack auth token) 13 | 14 | 1. Go to your Slack management page, and navigate to "Configure Apps" (or go to https://your-slack-workspace.slack.com/apps/manage) 15 | 1. Click on "Custom Integrations" 16 | 1. Click on "Bots" 17 | 1. Click on "Add to Slack" 18 | 1. Give your Slack bot a username (such as `spinnakerbot`) 19 | 1. Click "Add bot integration" 20 | 1. Copy the "API Token". Optionally, customize other settings on the Bot configuration. 21 | 1. Click "Save Integration" 22 | 23 | ## Add the Slack bot user to Spinnaker 24 | 25 | 1. SSH into your Minnaker instance 26 | 1. Run this command (replace `spinnakerbot` with your Slack bot's username) to add the Slack notification configuration. 27 | 28 | ```bash 29 | hal config notification slack edit --bot-name spinnakerbot --token 30 | ``` 31 | 32 | 1. Run this command to enable the Slack notification 33 | 34 | ``bash 35 | hal config notification slack enable 36 | ``` 37 | 38 | 1. Run this command to apply your changes 39 | 40 | ```bash 41 | hal deploy apply 42 | ``` 43 | 44 | ## Use the Slack notification 45 | 46 | In order to notify into a given Slack channel, the Slack bot should be invited into your Slack channel(s). Then, in a pipeline configuration, on the 'configuration' page, you can configure notifications to those channels for when your pipeline starts, completes, or fails, and you can additionally configure the same notifications on individual stages. 47 | -------------------------------------------------------------------------------- /pipelines/armory-basic-k8s-pipe.yaml: -------------------------------------------------------------------------------- 1 | # file: deploy-nginx.yaml 2 | apiVersion: pacrd.armory.spinnaker.io/v1alpha1 3 | kind: Pipeline 4 | metadata: 5 | name: basic-deploy-to-kubernetes 6 | spec: 7 | description: Basic nginx deploy to K3s 8 | application: &app-name armory-samples 9 | stages: 10 | - type: manualJudgment 11 | properties: 12 | name: This deploys locally on K3s. Want External K8s? 13 | refId: "1" 14 | failPipeline: true 15 | instructions: "Click Here -> Connect to Deployment Cluster?Watch Video and Configure Armory Agent
" 16 | - type: deployManifest 17 | properties: 18 | name: Deploy text manifest 19 | refId: "2" 20 | requisiteStageRefIds: ["1"] 21 | account: spinnaker 22 | cloudProvider: kubernetes 23 | moniker: 24 | app: *app-name 25 | skipExpressionEvaluation: true 26 | source: text 27 | comments: This is a test for weekhooks 28 | manifests: 29 | - | 30 | apiVersion: apps/v1 31 | kind: Deployment 32 | metadata: 33 | name: new-microservice 34 | namespace: prod 35 | labels: 36 | app: nginx 37 | spec: 38 | replicas: 2 39 | selector: 40 | matchLabels: 41 | app: nginx 42 | template: 43 | metadata: 44 | labels: 45 | app: nginx 46 | spec: 47 | containers: 48 | - name: nginx 49 | image: nginx:1.14.2 50 | ports: 51 | - containerPort: 80 52 | -------------------------------------------------------------------------------- /templates/archive/halyard-with-pvc.yml: -------------------------------------------------------------------------------- 1 | # This is currently not used 2 | --- 3 | apiVersion: v1 4 | kind: PersistentVolumeClaim 5 | metadata: 6 | name: hal-pvc 7 | labels: 8 | app: halyard 9 | namespace: spinnaker 10 | spec: 11 | accessModes: 12 | - ReadWriteOnce 13 | resources: 14 | requests: 15 | storage: 100Mi 16 | --- 17 | apiVersion: v1 18 | kind: PersistentVolumeClaim 19 | metadata: 20 | name: kube-pvc 21 | labels: 22 | app: halyard 23 | namespace: spinnaker 24 | spec: 25 | accessModes: 26 | - ReadWriteOnce 27 | resources: 28 | requests: 29 | storage: 10Mi 30 | --- 31 | apiVersion: apps/v1 32 | kind: StatefulSet 33 | metadata: 34 | name: halyard 35 | namespace: spinnaker 36 | spec: 37 | replicas: 1 38 | serviceName: halyard 39 | selector: 40 | matchLabels: 41 | app: halyard 42 | template: 43 | metadata: 44 | labels: 45 | app: halyard 46 | spec: 47 | containers: 48 | - name: halyard 49 | image: HALYARD_IMAGE 50 | volumeMounts: 51 | - name: hal 52 | mountPath: "/home/spinnaker/.hal" 53 | - name: kube 54 | mountPath: "/home/spinnaker/.kube" 55 | env: 56 | - name: HOME 57 | value: "/home/spinnaker" 58 | ports: 59 | - containerPort: 8064 60 | protocol: TCP 61 | readinessProbe: 62 | exec: 63 | command: 64 | - wget 65 | - --no-check-certificate 66 | - --spider 67 | - -q 68 | - http://localhost:8064/health 69 | securityContext: 70 | runAsUser: 1000 71 | runAsGroup: 65535 72 | # fsGroup: 65535 73 | volumes: 74 | - name: hal 75 | persistentVolumeClaim: 76 | claimName: hal-pvc 77 | - name: kube 78 | persistentVolumeClaim: 79 | claimName: kube-pvc 80 | -------------------------------------------------------------------------------- /scripts/addons/spinnaker_enable_canary.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ################################################################################ 4 | # Copyright 2020 Armory, Inc. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | ################################################################################ 18 | 19 | set -x 20 | set -e 21 | 22 | ###### 23 | hal config canary enable 24 | hal config canary prometheus enable 25 | hal config canary prometheus account add prometheus --base-url http://prometheus.default:9090/prometheus 26 | 27 | hal config canary aws enable 28 | # For some reason, Kayenta doesn't like using the same bucket as Front50, so we're setting up a different bucket 29 | # This will result in s3://kayenta/kayenta 30 | # TODO: Detect existing account 31 | echo "MINIO_PASSWORD" | hal config canary aws account add minio --bucket kayenta --root-folder kayenta --endpoint http://minio.spinnaker:9000 --access-key-id minio --secret-access-key 32 | hal config canary aws edit --s3-enabled=true 33 | 34 | hal config canary edit --default-metrics-store prometheus 35 | hal config canary edit --default-metrics-account prometheus 36 | hal config canary edit --default-storage-account minio 37 | 38 | # TODO: Detect existence of this 39 | # Extra blank lines are intentional 40 | tee -a /etc/spinnaker/.hal/default/profiles/gate-local.yml <<-'EOF' 41 | 42 | services: 43 | kayenta: 44 | canaryConfigStore: true 45 | 46 | EOF 47 | 48 | hal deploy apply -------------------------------------------------------------------------------- /pipelines/bootstrap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: pacrd.armory.spinnaker.io/v1alpha1 2 | kind: Application 3 | metadata: 4 | name: bootstrap 5 | spec: 6 | email: chad.tripod@armory.io 7 | description: Sample pipeline showing a blue/green deployment 8 | --- 9 | apiVersion: pacrd.armory.spinnaker.io/v1alpha1 10 | kind: Pipeline 11 | metadata: 12 | name: click-start-manual-execution 13 | spec: 14 | description: Click "Start Manual Execution" Link to the Right. Then see the "Armroy samples" Application 15 | application: &app-name bootstrap 16 | expectedArtifacts: 17 | - id: &manifest-repo-id pipelines 18 | displayName: manifest-repo 19 | matchArtifact: &manifest-repo-artifact 20 | type: git/repo 21 | properties: 22 | artifactAccount: gitrepo 23 | reference: https://github.com/armory/minnaker.git 24 | version: pacrd 25 | defaultArtifact: 26 | <<: *manifest-repo-artifact 27 | useDefaultArtifact: true 28 | usePriorArtifact: false 29 | stages: 30 | - type: bakeManifest 31 | properties: 32 | templateRenderer: KUSTOMIZE 33 | refId: "1" 34 | name: Render Kustomize Template 35 | kustomizeFilePath: "pipelines/kustomization.yaml" 36 | inputArtifact: 37 | id: *manifest-repo-id 38 | account: gitrepo 39 | expectedArtifacts: 40 | - id: &rendered-manifest-id rendered-manifest-id 41 | displayName: app-manifest 42 | useDefaultArtifact: false 43 | usePriorArtifact: false 44 | matchArtifact: 45 | type: embedded/base64 46 | properties: 47 | name: app-manifest 48 | - type: manualJudgment 49 | properties: 50 | name: Continue Deployment? 51 | refId: "2" 52 | requisiteStageRefIds: [ "1" ] 53 | - type: deployManifest 54 | properties: 55 | name: Deploy Application 56 | refId: "3" 57 | requisiteStageRefIds: ["2"] 58 | account: spinnaker 59 | cloudProvider: kubernetes 60 | source: artifact 61 | manifestArtifactAccount: embedded-artifact 62 | manifestArtifactId: *rendered-manifest-id 63 | moniker: 64 | app: *app-name 65 | namespaceOverride: spinnaker 66 | -------------------------------------------------------------------------------- /scripts/utils/switch_to_oss.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ################################################################################ 4 | # Copyright 2020 Armory, Inc. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | ################################################################################ 18 | 19 | set -e 20 | 21 | # Linux only 22 | 23 | PROJECT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )/../../" >/dev/null 2>&1 && pwd ) 24 | KUBERNETES_CONTEXT=default 25 | NAMESPACE=spinnaker 26 | BASE_DIR=/etc/spinnaker 27 | 28 | OLD_IMAGE=$(yq r ${BASE_DIR}/manifests/halyard.yml spec.template.spec.containers[0].image) 29 | if [[ ${OLD_IMAGE} =~ "armory" ]]; then 30 | echo ${OLD_IMAGE} > ${BASE_DIR}/armory_image 31 | else 32 | echo ${OLD_IMAGE} > ${BASE_DIR}/oss_image 33 | fi 34 | 35 | if [[ -f ${BASE_DIR}/oss_image ]]; then 36 | yq w -i ${BASE_DIR}/manifests/halyard.yml spec.template.spec.containers[0].image $(cat ${BASE_DIR}/oss_image) 37 | else 38 | yq w -i ${BASE_DIR}/manifests/halyard.yml spec.template.spec.containers[0].image gcr.io/spinnaker-marketplace/halyard:stable 39 | fi 40 | 41 | kubectl apply -f ${BASE_DIR}/manifests/halyard.yml 42 | 43 | sleep 5 44 | 45 | while [[ $(kubectl --context ${KUBERNETES_CONTEXT} get statefulset -n ${NAMESPACE} halyard -ojsonpath='{.status.readyReplicas}') -ne 1 ]]; 46 | do 47 | echo "Waiting for Halyard pod to start" 48 | sleep 5; 49 | done 50 | 51 | yq r ${BASE_DIR}/.hal/config deploymentConfigurations[0].armory >> ${BASE_DIR}/halconfig_armory 52 | yq d -i ${BASE_DIR}/.hal/config deploymentConfigurations[0].armory 53 | 54 | VERSION=$(kubectl --context ${KUBERNETES_CONTEXT} -n ${NAMESPACE} exec -i halyard-0 -- sh -c "hal version latest -q") 55 | kubectl --context ${KUBERNETES_CONTEXT} -n ${NAMESPACE} exec -i halyard-0 -- sh -c "hal config version edit --version ${VERSION}" 56 | kubectl --context ${KUBERNETES_CONTEXT} -n ${NAMESPACE} exec -i halyard-0 -- sh -c "hal deploy apply" -------------------------------------------------------------------------------- /scripts/refresh_endpoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ################################################################################ 4 | # Copyright 2020 Armory, Inc. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | ################################################################################ 18 | 19 | # Not (currently) designed for OSX 20 | 21 | # This is used to 'reset' a Minnaker instance. It regenerates the Minio and Gate passwords 22 | # Also, it will do the following with the public endpoint: 23 | # # If a new public endpoint is provided (with the flag -P), then the new endpoint will be used 24 | # # Otherwise, if the previous public endpoint was provided was a flag, that endpoint will be used 25 | # # Otherwise, the public endpoint will be re-detected 26 | 27 | # set -x 28 | set -e 29 | 30 | ##### Functions 31 | print_help () { 32 | set +x 33 | echo "Usage: refresh_endpoint.sh" 34 | echo " [-P|--public-endpoint\nCurrent Image: Get current image from hello-world-prod deployment (if it's valid), otherwise default to 'justinrlee/hello-world:monday'\nCurrent Instances: Get current replica count from hello-world-prod deployment (if it's valid), otherwise default to 4\nNew Image: Build from trigger.\n", 68 | "failOnFailedExpressions": true, 69 | "name": "Evaluate Variables", 70 | "refId": "3", 71 | "requisiteStageRefIds": [ 72 | "2", 73 | "5" 74 | ], 75 | "type": "evaluateVariables", 76 | "variables": [ 77 | { 78 | "key": "current_image", 79 | "value": "${#stage(\"Get Info\").status == \"FAILED_CONTINUE\" ? \"justinrlee/hello-world:monday\" : #stage(\"Get Info\").context.artifacts.^[type== \"docker/image\"].reference}" 80 | }, 81 | { 82 | "key": "current_instances", 83 | "value": "${#stage(\"Get Info\").status == \"FAILED_CONTINUE\" ? 4 : #stage(\"Get Info\").context.manifest.spec.replicas}" 84 | }, 85 | { 86 | "key": "new_image", 87 | "value": "justinrlee/hello-world:${trigger.parameters.tag == \"random\" ? new String[7]{\"monday\",\"tuesday\",\"wednesday\",\"thursday\",\"friday\",\"saturday\",\"sunday\"}[new java.util.Random().nextInt(7)] : trigger.parameters.tag}" 88 | }, 89 | { 90 | "key": "random_day", 91 | "value": "${new String[7]{\"monday\",\"tuesday\",\"wednesday\",\"thursday\",\"friday\",\"saturday\",\"sunday\"}[new java.util.Random().nextInt(7)]}" 92 | } 93 | ] 94 | }, 95 | { 96 | "account": "spinnaker", 97 | "cloudProvider": "kubernetes", 98 | "manifests": [ 99 | { 100 | "apiVersion": "apps/v1", 101 | "kind": "Deployment", 102 | "metadata": { 103 | "annotations": { 104 | "strategy.spinnaker.io/max-version-history": "2" 105 | }, 106 | "name": "hello-world-baseline" 107 | }, 108 | "spec": { 109 | "replicas": 1, 110 | "selector": { 111 | "matchLabels": { 112 | "app": "hello-world", 113 | "group": "baseline" 114 | } 115 | }, 116 | "template": { 117 | "metadata": { 118 | "annotations": { 119 | "prometheus.io/path": "/metrics", 120 | "prometheus.io/port": "8080", 121 | "prometheus.io/scrape": "true" 122 | }, 123 | "labels": { 124 | "app": "hello-world", 125 | "group": "baseline" 126 | } 127 | }, 128 | "spec": { 129 | "containers": [ 130 | { 131 | "image": "${current_image}", 132 | "imagePullPolicy": "Always", 133 | "name": "hello-world", 134 | "ports": [ 135 | { 136 | "containerPort": 8080 137 | } 138 | ] 139 | } 140 | ] 141 | } 142 | } 143 | } 144 | } 145 | ], 146 | "moniker": { 147 | "app": "democanary" 148 | }, 149 | "name": "Deploy Baseline", 150 | "namespaceOverride": "prod", 151 | "refId": "4", 152 | "requisiteStageRefIds": [ 153 | "3" 154 | ], 155 | "skipExpressionEvaluation": false, 156 | "source": "text", 157 | "trafficManagement": { 158 | "enabled": false, 159 | "options": { 160 | "enableTraffic": false, 161 | "services": [] 162 | } 163 | }, 164 | "type": "deployManifest" 165 | }, 166 | { 167 | "account": "spinnaker", 168 | "cloudProvider": "kubernetes", 169 | "manifests": [ 170 | { 171 | "apiVersion": "v1", 172 | "kind": "Service", 173 | "metadata": { 174 | "labels": { 175 | "app": "hello-world" 176 | }, 177 | "name": "hello-world" 178 | }, 179 | "spec": { 180 | "ports": [ 181 | { 182 | "name": "web", 183 | "port": 8080 184 | } 185 | ], 186 | "selector": { 187 | "app": "hello-world" 188 | } 189 | } 190 | }, 191 | { 192 | "apiVersion": "monitoring.coreos.com/v1", 193 | "kind": "ServiceMonitor", 194 | "metadata": { 195 | "name": "hello-world" 196 | }, 197 | "spec": { 198 | "endpoints": [ 199 | { 200 | "port": "web" 201 | } 202 | ], 203 | "namespaceSelector": { 204 | "any": true 205 | }, 206 | "podTargetLabels": [ 207 | "group", 208 | "app_version" 209 | ], 210 | "selector": { 211 | "matchLabels": { 212 | "app": "hello-world" 213 | } 214 | } 215 | } 216 | } 217 | ], 218 | "moniker": { 219 | "app": "democanary" 220 | }, 221 | "name": "Deploy Service and ServiceMonitor", 222 | "namespaceOverride": "prod", 223 | "refId": "5", 224 | "requisiteStageRefIds": [], 225 | "skipExpressionEvaluation": false, 226 | "source": "text", 227 | "trafficManagement": { 228 | "enabled": false, 229 | "options": { 230 | "enableTraffic": false 231 | } 232 | }, 233 | "type": "deployManifest" 234 | }, 235 | { 236 | "account": "spinnaker", 237 | "cloudProvider": "kubernetes", 238 | "manifests": [ 239 | { 240 | "apiVersion": "apps/v1", 241 | "kind": "Deployment", 242 | "metadata": { 243 | "annotations": { 244 | "strategy.spinnaker.io/max-version-history": "2" 245 | }, 246 | "name": "hello-world-canary" 247 | }, 248 | "spec": { 249 | "replicas": 1, 250 | "selector": { 251 | "matchLabels": { 252 | "app": "hello-world", 253 | "group": "canary" 254 | } 255 | }, 256 | "template": { 257 | "metadata": { 258 | "annotations": { 259 | "prometheus.io/path": "/metrics", 260 | "prometheus.io/port": "8080", 261 | "prometheus.io/scrape": "true" 262 | }, 263 | "labels": { 264 | "app": "hello-world", 265 | "group": "canary" 266 | } 267 | }, 268 | "spec": { 269 | "containers": [ 270 | { 271 | "image": "${new_image}", 272 | "imagePullPolicy": "Always", 273 | "name": "hello-world", 274 | "ports": [ 275 | { 276 | "containerPort": 8080 277 | } 278 | ] 279 | } 280 | ] 281 | } 282 | } 283 | } 284 | } 285 | ], 286 | "moniker": { 287 | "app": "democanary" 288 | }, 289 | "name": "Deploy Canary", 290 | "namespaceOverride": "prod", 291 | "refId": "6", 292 | "requisiteStageRefIds": [ 293 | "3" 294 | ], 295 | "skipExpressionEvaluation": false, 296 | "source": "text", 297 | "trafficManagement": { 298 | "enabled": false, 299 | "options": { 300 | "enableTraffic": false, 301 | "services": [] 302 | } 303 | }, 304 | "type": "deployManifest" 305 | }, 306 | { 307 | "name": "Gather", 308 | "refId": "7", 309 | "requisiteStageRefIds": [ 310 | "4", 311 | "6" 312 | ], 313 | "type": "wait", 314 | "waitTime": 1 315 | }, 316 | { 317 | "name": "Wait", 318 | "comments": "This is a dummy stage that could be substituted with other testing stages", 319 | "refId": "8", 320 | "requisiteStageRefIds": [ 321 | "7" 322 | ], 323 | "type": "wait", 324 | "waitTime": 2 325 | }, 326 | { 327 | "completeOtherBranchesThenFail": false, 328 | "continuePipeline": true, 329 | "failPipeline": false, 330 | "comments": "This is a manual judgment will only occur if the canary fails; otherwise, the app will be promoted automatically", 331 | "instructions": "Click \"Continue\" to promote and \"Stop\" to not promote.", 332 | "judgmentInputs": [], 333 | "name": "Manual Judgment", 334 | "notifications": [], 335 | "refId": "9", 336 | "requisiteStageRefIds": [ 337 | "8", 338 | "13" 339 | ], 340 | "stageEnabled": { 341 | "expression": "${#stage(\"Canary Analysis\").status != \"SUCCEEDED\"}", 342 | "type": "expression" 343 | }, 344 | "type": "manualJudgment" 345 | }, 346 | { 347 | "account": "spinnaker", 348 | "app": "democanary", 349 | "cloudProvider": "kubernetes", 350 | "location": "prod", 351 | "manifestName": "deployment hello-world-canary", 352 | "mode": "static", 353 | "name": "Destroy Canary", 354 | "options": { 355 | "cascading": true 356 | }, 357 | "refId": "10", 358 | "requisiteStageRefIds": [ 359 | "9" 360 | ], 361 | "type": "deleteManifest" 362 | }, 363 | { 364 | "account": "spinnaker", 365 | "app": "democanary", 366 | "cloudProvider": "kubernetes", 367 | "location": "prod", 368 | "manifestName": "deployment hello-world-baseline", 369 | "mode": "static", 370 | "name": "Destroy Baseline", 371 | "options": { 372 | "cascading": true 373 | }, 374 | "refId": "11", 375 | "requisiteStageRefIds": [ 376 | "9" 377 | ], 378 | "type": "deleteManifest" 379 | }, 380 | { 381 | "account": "spinnaker", 382 | "cloudProvider": "kubernetes", 383 | "manifests": [ 384 | { 385 | "apiVersion": "apps/v1", 386 | "kind": "Deployment", 387 | "metadata": { 388 | "annotations": { 389 | "strategy.spinnaker.io/max-version-history": "4" 390 | }, 391 | "name": "hello-world-prod" 392 | }, 393 | "spec": { 394 | "replicas": "${current_instances.intValue()}", 395 | "selector": { 396 | "matchLabels": { 397 | "app": "hello-world", 398 | "group": "prod" 399 | } 400 | }, 401 | "template": { 402 | "metadata": { 403 | "annotations": { 404 | "prometheus.io/path": "/metrics", 405 | "prometheus.io/port": "8080", 406 | "prometheus.io/scrape": "true" 407 | }, 408 | "labels": { 409 | "app": "hello-world", 410 | "group": "prod" 411 | } 412 | }, 413 | "spec": { 414 | "containers": [ 415 | { 416 | "image": "${new_image}", 417 | "imagePullPolicy": "Always", 418 | "name": "hello-world", 419 | "ports": [ 420 | { 421 | "containerPort": 8080 422 | } 423 | ] 424 | } 425 | ] 426 | } 427 | } 428 | } 429 | } 430 | ], 431 | "moniker": { 432 | "app": "democanary" 433 | }, 434 | "name": "Promote Prod", 435 | "namespaceOverride": "prod", 436 | "refId": "12", 437 | "requisiteStageRefIds": [ 438 | "9" 439 | ], 440 | "skipExpressionEvaluation": false, 441 | "source": "text", 442 | "stageEnabled": { 443 | "expression": "${#stage(\"Manual Judgment\").context.judgmentStatus != \"stop\"}", 444 | "type": "expression" 445 | }, 446 | "trafficManagement": { 447 | "enabled": false, 448 | "options": { 449 | "enableTraffic": false, 450 | "services": [] 451 | } 452 | }, 453 | "type": "deployManifest" 454 | }, 455 | { 456 | "analysisType": "realTime", 457 | "canaryConfig": { 458 | "beginCanaryAnalysisAfterMins": "1", 459 | "canaryAnalysisIntervalMins": "4", 460 | "canaryConfigId": "__CANARY_CONFIG_UUID__", 461 | "lifetimeDuration": "PT0H20M", 462 | "metricsAccountName": "prometheus", 463 | "scopes": [ 464 | { 465 | "controlLocation": "prod", 466 | "controlScope": "baseline", 467 | "experimentLocation": "prod", 468 | "experimentScope": "canary", 469 | "extendedScopeParams": {}, 470 | "scopeName": "default", 471 | "step": 5 472 | } 473 | ], 474 | "scoreThresholds": { 475 | "marginal": "0", 476 | "pass": "70" 477 | }, 478 | "storageAccountName": "minio" 479 | }, 480 | "completeOtherBranchesThenFail": false, 481 | "continuePipeline": true, 482 | "failPipeline": false, 483 | "name": "Canary Analysis", 484 | "refId": "13", 485 | "requisiteStageRefIds": [ 486 | "7" 487 | ], 488 | "type": "kayentaCanary" 489 | } 490 | ], 491 | "triggers": [ 492 | ] 493 | } -------------------------------------------------------------------------------- /templates/addons/demo/demok8s/pipelines/PIPELINE_UUID/pipeline-metadata.json.tmpl: -------------------------------------------------------------------------------- 1 | { 2 | "application": "demok8s", 3 | "name": "Kubernetes Demo", 4 | "id": "__PIPELINE_UUID__", 5 | "updateTs": "__TIMESTAMP__", 6 | "description": "This is a sample pipeline that does the following:\n* Sets up a number of Kubernetes Services and Ingresses (using the default Traefik Ingress controller included with K3s)\n* Deploys a hello world Deployment to the dev namespace (called 'demok8s')\n* Pauses for approval before deploying to test\n* Deploys the hello world Deployment to the test namespace (also called 'demok8s')\n* Runs, in parallel, a webhook and a wait stage (these could be substituted for calls out to other test tools or endpoints)\n* Pauses for approval before deploying to prod\n* Deploys the hello world app as a ReplicaSet to the prod namespace (also called 'demok8s'), using the Spinnaker blue/green (red/black) capability.", 7 | "keepWaitingPipelines": false, 8 | "lastModifiedBy": "demo", 9 | "limitConcurrent": true, 10 | "parameterConfig": [ 11 | { 12 | "default": "monday", 13 | "description": "", 14 | "hasOptions": true, 15 | "label": "", 16 | "name": "tag", 17 | "options": [ 18 | { 19 | "value": "monday" 20 | }, 21 | { 22 | "value": "tuesday" 23 | }, 24 | { 25 | "value": "wednesday" 26 | }, 27 | { 28 | "value": "thursday" 29 | }, 30 | { 31 | "value": "friday" 32 | }, 33 | { 34 | "value": "saturday" 35 | }, 36 | { 37 | "value": "sunday" 38 | } 39 | ], 40 | "pinned": false, 41 | "required": true 42 | } 43 | ], 44 | "stages": [ 45 | { 46 | "account": "spinnaker", 47 | "cloudProvider": "kubernetes", 48 | "comments": "
Deploying to the dev environment (access the app at https://MINNAKER_URL/dev/demok8s)
\n\nThe endpoint is accessed through a Kubernetes Ingress (listening on path /dev/demok8s) and accompany Kubernetes Service, both in the \"dev\" namespace.
", 49 | "manifests": [ 50 | { 51 | "apiVersion": "apps/v1", 52 | "kind": "Deployment", 53 | "metadata": { 54 | "name": "demok8s" 55 | }, 56 | "spec": { 57 | "replicas": 3, 58 | "selector": { 59 | "matchLabels": { 60 | "app": "demok8s" 61 | } 62 | }, 63 | "template": { 64 | "metadata": { 65 | "labels": { 66 | "app": "demok8s", 67 | "lb": "demok8s" 68 | } 69 | }, 70 | "spec": { 71 | "containers": [ 72 | { 73 | "image": "justinrlee/nginx:${parameters[\"tag\"]}", 74 | "name": "primary", 75 | "ports": [ 76 | { 77 | "containerPort": 80 78 | } 79 | ] 80 | } 81 | ] 82 | } 83 | } 84 | } 85 | } 86 | ], 87 | "moniker": { 88 | "app": "demok8s" 89 | }, 90 | "name": "Deploy Dev", 91 | "namespaceOverride": "dev", 92 | "refId": "1", 93 | "requisiteStageRefIds": [], 94 | "skipExpressionEvaluation": false, 95 | "source": "text", 96 | "trafficManagement": { 97 | "enabled": false, 98 | "options": { 99 | "enableTraffic": false, 100 | "services": [] 101 | } 102 | }, 103 | "type": "deployManifest" 104 | }, 105 | { 106 | "failPipeline": true, 107 | "instructions": "Please verify Dev (https://MINNAKER_URL/dev/demok8s) and click 'Continue' to continue deploying to Test.", 108 | "judgmentInputs": [], 109 | "name": "Manual Judgment: Deploy to Test", 110 | "notifications": [], 111 | "refId": "2", 112 | "requisiteStageRefIds": [ 113 | "1", 114 | "8", 115 | "9" 116 | ], 117 | "type": "manualJudgment" 118 | }, 119 | { 120 | "account": "spinnaker", 121 | "cloudProvider": "kubernetes", 122 | "comments": "Deploying to the test environment (access the app at https://MINNAKER_URL/test/demok8s)
\n\nThe endpoint is accessed through a Kubernetes Ingress (listening on path /test/demok8s) and accompany Kubernetes Service, both in the \"test\" namespace.
", 123 | "manifests": [ 124 | { 125 | "apiVersion": "apps/v1", 126 | "kind": "Deployment", 127 | "metadata": { 128 | "name": "demok8s" 129 | }, 130 | "spec": { 131 | "replicas": 3, 132 | "selector": { 133 | "matchLabels": { 134 | "app": "demok8s" 135 | } 136 | }, 137 | "template": { 138 | "metadata": { 139 | "labels": { 140 | "app": "demok8s", 141 | "lb": "demok8s" 142 | } 143 | }, 144 | "spec": { 145 | "containers": [ 146 | { 147 | "image": "justinrlee/nginx:${parameters[\"tag\"]}", 148 | "name": "primary", 149 | "ports": [ 150 | { 151 | "containerPort": 80 152 | } 153 | ] 154 | } 155 | ] 156 | } 157 | } 158 | } 159 | } 160 | ], 161 | "moniker": { 162 | "app": "demok8s" 163 | }, 164 | "name": "Deploy Test", 165 | "namespaceOverride": "test", 166 | "refId": "3", 167 | "requisiteStageRefIds": [ 168 | "2" 169 | ], 170 | "skipExpressionEvaluation": false, 171 | "source": "text", 172 | "trafficManagement": { 173 | "enabled": false, 174 | "options": { 175 | "enableTraffic": false, 176 | "services": [] 177 | } 178 | }, 179 | "type": "deployManifest" 180 | }, 181 | { 182 | "name": "Wait", 183 | "refId": "4", 184 | "requisiteStageRefIds": [ 185 | "3" 186 | ], 187 | "type": "wait", 188 | "waitTime": 30 189 | }, 190 | { 191 | "failPipeline": true, 192 | "instructions": "Please verify Test (https://MINNAKER_URL/test/demok8s) and click 'Continue' to continue deploying to Prod", 193 | "judgmentInputs": [], 194 | "name": "Manual Judgment: Deploy to Prod", 195 | "notifications": [], 196 | "refId": "5", 197 | "requisiteStageRefIds": [ 198 | "4", 199 | "7" 200 | ], 201 | "type": "manualJudgment" 202 | }, 203 | { 204 | "account": "spinnaker", 205 | "cloudProvider": "kubernetes", 206 | "comments": "Deploying to the prod environment (access the app at https://MINNAKER_URL/prod/demok8s)
\n\nThe endpoint is accessed through a Kubernetes Ingress (listening on path /prod/demok8s) and accompany Kubernetes Service, both in the \"prod\" namespace.
", 207 | "manifests": [ 208 | { 209 | "apiVersion": "apps/v1", 210 | "kind": "ReplicaSet", 211 | "metadata": { 212 | "name": "demok8s" 213 | }, 214 | "spec": { 215 | "replicas": 3, 216 | "selector": { 217 | "matchLabels": { 218 | "app": "demok8s" 219 | } 220 | }, 221 | "template": { 222 | "metadata": { 223 | "labels": { 224 | "app": "demok8s" 225 | } 226 | }, 227 | "spec": { 228 | "containers": [ 229 | { 230 | "image": "justinrlee/nginx:${parameters[\"tag\"]}", 231 | "name": "primary", 232 | "ports": [ 233 | { 234 | "containerPort": 80, 235 | "protocol": "TCP" 236 | } 237 | ] 238 | } 239 | ] 240 | } 241 | } 242 | } 243 | } 244 | ], 245 | "moniker": { 246 | "app": "demok8s" 247 | }, 248 | "name": "Deploy Prod (Blue/Green)", 249 | "namespaceOverride": "prod", 250 | "refId": "6", 251 | "requisiteStageRefIds": [ 252 | "5" 253 | ], 254 | "skipExpressionEvaluation": false, 255 | "source": "text", 256 | "trafficManagement": { 257 | "enabled": true, 258 | "options": { 259 | "enableTraffic": true, 260 | "namespace": "prod", 261 | "services": [ 262 | "service demok8s" 263 | ], 264 | "strategy": "redblack" 265 | } 266 | }, 267 | "type": "deployManifest" 268 | }, 269 | { 270 | "method": "GET", 271 | "name": "Webhook", 272 | "refId": "7", 273 | "requisiteStageRefIds": [ 274 | "3" 275 | ], 276 | "statusUrlResolution": "getMethod", 277 | "type": "webhook", 278 | "url": "https://www.google.com/" 279 | }, 280 | { 281 | "account": "spinnaker", 282 | "cloudProvider": "kubernetes", 283 | "manifests": [ 284 | { 285 | "apiVersion": "v1", 286 | "kind": "Service", 287 | "metadata": { 288 | "name": "demok8s", 289 | "namespace": "dev" 290 | }, 291 | "spec": { 292 | "ports": [ 293 | { 294 | "name": "http", 295 | "port": 80, 296 | "protocol": "TCP", 297 | "targetPort": 80 298 | } 299 | ], 300 | "selector": { 301 | "lb": "demok8s" 302 | } 303 | } 304 | }, 305 | { 306 | "apiVersion": "extensions/v1beta1", 307 | "kind": "Ingress", 308 | "metadata": { 309 | "annotations": { 310 | "nginx.ingress.kubernetes.io/rewrite-target": "/", 311 | "traefik.ingress.kubernetes.io/rule-type": "PathPrefixStrip" 312 | }, 313 | "labels": { 314 | "app": "demok8s" 315 | }, 316 | "name": "demok8s", 317 | "namespace": "dev" 318 | }, 319 | "spec": { 320 | "rules": [ 321 | { 322 | "http": { 323 | "paths": [ 324 | { 325 | "backend": { 326 | "serviceName": "demok8s", 327 | "servicePort": "http" 328 | }, 329 | "path": "/dev/demok8s" 330 | } 331 | ] 332 | } 333 | } 334 | ] 335 | } 336 | } 337 | ], 338 | "moniker": { 339 | "app": "demok8s" 340 | }, 341 | "name": "Deploy Dev Service and Ingress", 342 | "namespaceOverride": "", 343 | "refId": "8", 344 | "requisiteStageRefIds": [], 345 | "skipExpressionEvaluation": false, 346 | "source": "text", 347 | "trafficManagement": { 348 | "enabled": false, 349 | "options": { 350 | "enableTraffic": false, 351 | "services": [] 352 | } 353 | }, 354 | "type": "deployManifest" 355 | }, 356 | { 357 | "account": "spinnaker", 358 | "cloudProvider": "kubernetes", 359 | "manifests": [ 360 | { 361 | "apiVersion": "v1", 362 | "kind": "Service", 363 | "metadata": { 364 | "name": "demok8s", 365 | "namespace": "test" 366 | }, 367 | "spec": { 368 | "ports": [ 369 | { 370 | "name": "http", 371 | "port": 80, 372 | "protocol": "TCP", 373 | "targetPort": 80 374 | } 375 | ], 376 | "selector": { 377 | "lb": "demok8s" 378 | } 379 | } 380 | }, 381 | { 382 | "apiVersion": "extensions/v1beta1", 383 | "kind": "Ingress", 384 | "metadata": { 385 | "annotations": { 386 | "nginx.ingress.kubernetes.io/rewrite-target": "/", 387 | "traefik.ingress.kubernetes.io/rule-type": "PathPrefixStrip" 388 | }, 389 | "labels": { 390 | "app": "demok8s" 391 | }, 392 | "name": "demok8s", 393 | "namespace": "test" 394 | }, 395 | "spec": { 396 | "rules": [ 397 | { 398 | "http": { 399 | "paths": [ 400 | { 401 | "backend": { 402 | "serviceName": "demok8s", 403 | "servicePort": "http" 404 | }, 405 | "path": "/test/demok8s" 406 | } 407 | ] 408 | } 409 | } 410 | ] 411 | } 412 | } 413 | ], 414 | "moniker": { 415 | "app": "demok8s" 416 | }, 417 | "name": "Deploy Test Service and Ingress", 418 | "refId": "9", 419 | "requisiteStageRefIds": [], 420 | "skipExpressionEvaluation": false, 421 | "source": "text", 422 | "trafficManagement": { 423 | "enabled": false, 424 | "options": { 425 | "enableTraffic": false, 426 | "services": [] 427 | } 428 | }, 429 | "type": "deployManifest" 430 | }, 431 | { 432 | "account": "spinnaker", 433 | "cloudProvider": "kubernetes", 434 | "manifests": [ 435 | { 436 | "apiVersion": "v1", 437 | "kind": "Service", 438 | "metadata": { 439 | "name": "demok8s", 440 | "namespace": "prod" 441 | }, 442 | "spec": { 443 | "ports": [ 444 | { 445 | "name": "http", 446 | "port": 80, 447 | "protocol": "TCP", 448 | "targetPort": 80 449 | } 450 | ], 451 | "selector": { 452 | "lb": "demok8s" 453 | } 454 | } 455 | }, 456 | { 457 | "apiVersion": "extensions/v1beta1", 458 | "kind": "Ingress", 459 | "metadata": { 460 | "annotations": { 461 | "nginx.ingress.kubernetes.io/rewrite-target": "/", 462 | "traefik.ingress.kubernetes.io/rule-type": "PathPrefixStrip" 463 | }, 464 | "labels": { 465 | "app": "demok8s" 466 | }, 467 | "name": "demok8s", 468 | "namespace": "prod" 469 | }, 470 | "spec": { 471 | "rules": [ 472 | { 473 | "http": { 474 | "paths": [ 475 | { 476 | "backend": { 477 | "serviceName": "demok8s", 478 | "servicePort": "http" 479 | }, 480 | "path": "/prod/demok8s" 481 | } 482 | ] 483 | } 484 | } 485 | ] 486 | } 487 | } 488 | ], 489 | "moniker": { 490 | "app": "demok8s" 491 | }, 492 | "name": "Deploy Prod Service and Ingress", 493 | "namespaceOverride": "", 494 | "refId": "10", 495 | "requisiteStageRefIds": [], 496 | "skipExpressionEvaluation": false, 497 | "source": "text", 498 | "trafficManagement": { 499 | "enabled": false, 500 | "options": { 501 | "enableTraffic": false, 502 | "services": [] 503 | } 504 | }, 505 | "type": "deployManifest" 506 | } 507 | ], 508 | "triggers": [] 509 | } -------------------------------------------------------------------------------- /guides/first-pipeline-jenkins.md: -------------------------------------------------------------------------------- 1 | # Deploy a Jenkins-built container to Kubernetes with Spinnaker (with ECR) 2 | 3 | ### This document is still in *draft* form 4 | 5 | In this codelab, we will perform the following: 6 | 7 | * Configure Spinnaker with a GitHub credential 8 | 9 | Then, we'll build out the process to do the following: 10 | * Jenkins build a Docker image 11 | * Jenkins push the Docker image to ECR (alternately, to Docker Hub) 12 | * Jenkins send a webhook to Spinnaker to trigger a Spinnaker pipeline, with a Docker image artifact 13 | * Spinnaker receive the webhook, pull a Kubernets manifest from GitHub, hydrate the Docker image, and deploy the manifest to a Kubernetes cluster 14 | 15 | We assume the following in this document: 16 | * You have the following set up and configured: 17 | * A Kubernetes cluster 18 | * A Jenkins instance 19 | * A Spinnaker instance that has access to your Kubernetes cluster 20 | * A GitHub account or a GitHub Enterprise instance with an account 21 | * Jenkins slaves are configured with the Docker daemon (to build Docker images) 22 | * Jenkins slaves have credentials to push to your Docker registry of choice 23 | * Your Kubernetes cluster is able to run images from your Docker registry 24 | 25 | ## Set up 26 | ### Configure Spinnaker with GitHub credentials 27 | 28 | (OSS documentation for this is here: https://www.spinnaker.io/setup/artifacts/github/) 29 | 30 | First, create a credential for Spinnaker to use to access GitHub: 31 | 32 | * In your GitHub, go to Settings (click on your user icon in the top right) > Developer Settings > Personal Access Tokens 33 | * Click "Generate new token" 34 | * Give your token a name, and give it the 'repo' access 35 | * Copy the token down 36 | 37 | Then, using Halyard, add the credential to Spinnaker as "GitHub artifact Account" (these all take place in Halyard): 38 | 39 | * In your Halyard, enable the artifacts feature: 40 | 41 | ```bash 42 | hal config features edit --artifacts true 43 | ``` 44 | 45 | * In your Halyard, enable the new artifact UI feature: 46 | 47 | ```bash 48 | hal config features edit --artifacts-rewrite true 49 | ``` 50 | 51 | * Enable the GitHub artifact account type: 52 | 53 | ```bash 54 | hal config artifact github enable 55 | ``` 56 | 57 | * Add the credential as a "GitHub Artifact Account": 58 | 59 | *You will be prompted for a token; enter your token at the prompt.* 60 | 61 | ```bash 62 | hal config artifact github account add my-github-credential \ 63 | --token 64 | ``` 65 | 66 | * Appl (Deploy) your changes: 67 | 68 | ```bash 69 | hal deploy apply 70 | ``` 71 | 72 | ## Create an ECR Repository 73 | 74 | * In the AWS console, go to Compute > ECR 75 | * Click "Create repository" 76 | * Give it a name and namespace (for example, hello-world/nginx) 77 | * You'll get a repository formatted like this: `111122223333.dkr.ecr.us-west-2.amazonaws.com/hello-world/nginx` 78 | 79 | ## Configure Jenkins to push to the repository 80 | 81 | ### Set up Cross-Account Access 82 | 83 | Assuming Jenkins is running in a different AWS account from your ECR repository, you'll need to set up cross-account access. 84 | 85 | * Get the AWS account ID for the AWS account where Jenkins is running (you can use the command `aws sts get-caller-identity` to see what account you're accesssing from; for example, an ARN of `arn:aws:sts::222233334444:assumed-role/ec2-role/i-00001111222233334` means you're in `222233334444`) 86 | * Log into to the AWS console account where your ECR repository exists, and go to Compute > ECR 87 | * Click on your repository 88 | * On the left side, click on "Permissions" 89 | * Edit the policy JSON to include this: 90 | 91 | ```json 92 | { 93 | "Version": "2008-10-17", 94 | "Statement": [ 95 | { 96 | "Sid": "AllowCrossAccountPush", 97 | "Effect": "Allow", 98 | "Principal": { 99 | "AWS": "arn:aws:iam::222233334444:root" 100 | }, 101 | "Action": [ 102 | "ecr:BatchCheckLayerAvailability", 103 | "ecr:CompleteLayerUpload", 104 | "ecr:GetDownloadUrlForLayer", 105 | "ecr:InitiateLayerUpload", 106 | "ecr:PutImage", 107 | "ecr:UploadLayerPart" 108 | ] 109 | } 110 | ] 111 | } 112 | ``` 113 | 114 | *This will allow entities in the `222233334444` AWS account to push to this repo* 115 | 116 | * Save your changes 117 | 118 | ## Set up ECR Repository Access 119 | 120 | **There are many ways to do this; this is an insecure, temporary way to do this (ideally you'd set up use an AWS helper function to dynamically generate Docker creds)** 121 | 122 | The machine where you are doing Docker builds will need permissions and credentials to push to your ECR repository. In this case, this will likely be the Jenkins slave where your Docker builds will be taking place. 123 | 124 | * If you don't have an IAM role attached to the EC2 instance, go to the instance in the EC2 console, add an EC2 IAM role (either use an existing role, or create a new role), and add the `AmazonEC2ContainerRegistryFullAccess` policy to the role. 125 | 126 | ## Build the Docker image in Jenkins 127 | 128 | *Something very similar to this could be achieved with the Docker plugin, or whatever other Docker build and push mechanism your organization uses. This is meant to be more illustrative of the process than efficient; you can tweak this significantly with better build triggers, pipelines, plugins, and so forth.* 129 | 130 | In your Jenkins instance, create a new item of type "Freestyle project". Set up a "Build" step of type "Shell" with something like this: 131 | 132 | ```bash 133 | # This is a basic 'hello world' index page for nginx 134 | tee index.html <<-'EOF' 135 | hello world 136 | EOF 137 | 138 | # This is a basic Dockerfile that starts with nginx, and adds our hello world page 139 | tee Dockerfile <<-'EOF' 140 | FROM nginx:latest 141 | COPY index.html /usr/share/nginx/html/index.html 142 | EOF 143 | 144 | TAG=$(date +%s) 145 | 146 | # This removes any AWS creds if they're present in the environment; remove this if you want to use the creds baked into Jenkins 147 | 148 | unset AWS_ACCESS_KEY_ID 149 | unset AWS_SECRET_ACCESS_KEY 150 | 151 | # Replace 111122223333 with the account ID where your ECR repo is, and the region with the region where your ECR repo is 152 | $(aws ecr get-login --no-include-email --region us-west-2 --registry-ids 111122223333) 153 | 154 | # Replace 111122223333 with the account ID where your ECR repo is, and the region with the region where your ECR repo is 155 | docker build . -t 111122223333.dkr.ecr.us-west-2.amazonaws.com/hello-world/nginx:${TAG} 156 | 157 | # Replace 111122223333 with the account ID where your ECR repo is, and the region with the region where your ECR repo is 158 | docker push 111122223333.dkr.ecr.us-west-2.amazonaws.com/hello-world/nginx:${TAG} 159 | ``` 160 | 161 | Build your Jenkins job, and you should see a new Docker image tag show up in your ECR repo. 162 | 163 | ## Deploy an initial (static) manifest from Spinnaker 164 | 165 | * In Spinnaker, create a new application, then a new pipeline 166 | * Add a stage "Deploy (Manifest)" 167 | * Select your Kubernetes cluster from the Account drop down 168 | * Select the "Override Namespace" checkbox, and select a namespace that Spinnaker is allowed to deploy to 169 | * In the manifest, put this (replace the image with the produced image and tag) 170 | 171 | ```yml 172 | apiVersion: apps/v1 173 | kind: Deployment 174 | metadata: 175 | name: hello-today 176 | spec: 177 | replicas: 3 178 | selector: 179 | matchLabels: 180 | app: hello-today 181 | template: 182 | metadata: 183 | labels: 184 | app: hello-today 185 | lb: hello-today 186 | spec: 187 | containers: 188 | - image: '111122223333.dkr.ecr.us-west-2.amazonaws.com/hello-world/nginx:1581376100' 189 | name: primary 190 | ports: 191 | - containerPort: 80 192 | ``` 193 | 194 | Go to your pipelines page, and trigger the pipeline. Verify that it deploys (this will validate that Kubernetes can run images from your ECR repo) 195 | 196 | This should do the following: 197 | * Start the Spinnaker pipeline 198 | * Deploy the manifest 199 | * Wait for the pods to be fully up 200 | 201 | ## Update the manifest to use a dynamic image, and add a trigger with a default tag 202 | 203 | * Go back to the pipeline configuration. 204 | * In the pipeline stage UI, click on "Configuration" on the left 205 | * Click "Add Trigger" 206 | * Select "Webhook" for the "Type" 207 | * Add "hello-world" to the "source". This will create a URL like https://your-spinnaker-url/api/v1/webhooks/webhook/hello-world - this is the webhook endpoint used to trigger the pipeline. Remember this URL. 208 | * Add a payload constraint with a "key" of "secret" and a "value" of "my-secret-value" 209 | * Click on "Artifact Constraints" > Define a new artifact 210 | * Enter these values: 211 | * Account: "custom-artifact" 212 | * Type: "docker/image" 213 | * Name: "hello-world/nginx" 214 | * Check the "Use default artifact" checkbox. Enter these values: 215 | * Account: "custom-artifact" 216 | * Type: "docker/image" 217 | * Name: "hello-world/nginx" 218 | * Reference: "111122223333.dkr.ecr.us-west-2.amazonaws.com/hello-world/nginx:1581376100" (replace with a valid tag) 219 | * The artifact will result in a "Display Name" (like "mean-eel-993") 220 | * Navigate back to the "Deploy (Manifest)" stage, and make these changes: 221 | * Change the image field in the manifest to be just "hello-world/nginx" 222 | * Add a "Required artifacts to bind" indicating the display name of your artifact 223 | 224 | Your full manifest should look like this: 225 | 226 | ```yaml 227 | apiVersion: apps/v1 228 | kind: Deployment 229 | metadata: 230 | name: hello-today 231 | spec: 232 | replicas: 3 233 | selector: 234 | matchLabels: 235 | app: hello-today 236 | template: 237 | metadata: 238 | labels: 239 | app: hello-today 240 | lb: hello-today 241 | spec: 242 | containers: 243 | - image: hello-world/nginx 244 | name: primary 245 | ports: 246 | - containerPort: 80 247 | ``` 248 | 249 | Trigger the pipeline, and ensure the Docker image in the hydrated manifest is the fully qualified manifest 250 | 251 | Here's what this is doing: 252 | * When you run the pipeline, it's looking an input artifact matching this pattern: 253 | ```json 254 | { 255 | "type": "docker/image", 256 | "name": "hello-world/nginx" 257 | } 258 | ``` 259 | 260 | * Since you are triggering the pipeline manually, it's not finding the input artifact, so it's using the default artifact: 261 | ```json 262 | { 263 | "type": "docker/image", 264 | "name": "hello-world/nginx", 265 | "reference": "111122223333.dkr.ecr.us-west-2.amazonaws.com/hello-world/nginx:1581376505" 266 | } 267 | ``` 268 | 269 | * Because your manifest is configured to "Bind" the artifact, it will look for images populated with "hello-world/nginx", and replacing them with the "found" reference (in this case, the reference from the default artifact) before the deployment. 270 | 271 | This should do the following: 272 | * Start the Spinnaker pipeline 273 | * Parse the default artifact 274 | * Replace the `hello-world/nginx` with the reference from your passed-in artifact 275 | * Deploy the hydrated manifest 276 | * Wait for the pods to be fully up 277 | 278 | ## Trigger the pipeline from a CLI 279 | 280 | Do another build, and get another Docker image. For example: `111122223333.dkr.ecr.us-west-2.amazonaws.com/hello-world/nginx:1581376505` (note the different tag). 281 | 282 | Then, using the URL from above, as well as the key/value pair, do this from a shell terminal: 283 | 284 | ```bash 285 | tee body.json <<-EOF 286 | { 287 | "secret": "my-secret-value", 288 | "artifacts": [ 289 | { 290 | "type": "docker/image", 291 | "name": "hello-world/nginx", 292 | "reference": "111122223333.dkr.ecr.us-west-2.amazonaws.com/hello-world/nginx:1581376505" 293 | } 294 | ] 295 | } 296 | EOF 297 | 298 | curl -k -X POST \ 299 | -H 'content-type:application/json' \ 300 | -d @body.json \ 301 | https://your-spinnaker-url/api/v1/webhooks/webhook/hello-world 302 | ``` 303 | 304 | Go to Spinnaker, and your pipeline should trigger with the new tag (check the deployed manifest) 305 | 306 | This should do the following: 307 | * Start the Spinnaker pipeline 308 | * Parse the artifact with your generated tag 309 | * Pull the Kubernetes manifest from GitHub (Enterprise) 310 | * Replace the `hello-world/nginx` with the reference from your passed-in artifact 311 | * Deploy the hydrated manifest 312 | * Wait for the pods to be fully up 313 | 314 | ## Trigger the pipeline from Jenkins 315 | 316 | Go back into Jenkins, and add the above curl command to the end of your shell command. Replace the tag with your dynamically generated tag, so it'll look something like this: 317 | 318 | ```bash 319 | tee body.json <<-EOF 320 | { 321 | "secret": "my-secret-value", 322 | "artifacts": [ 323 | { 324 | "type": "docker/image", 325 | "name": "hello-world/nginx", 326 | "reference": "111122223333.dkr.ecr.us-west-2.amazonaws.com/hello-world/nginx:${TAG}" 327 | } 328 | ] 329 | } 330 | EOF 331 | 332 | curl -k -X POST \ 333 | -H 'content-type:application/json' \ 334 | -d @body.json \ 335 | https://your-spinnaker-url/api/v1/webhooks/webhook/hello-world 336 | ``` 337 | 338 | Trigger your Jenkins build, and it should do the following: 339 | * Build a new Docker image 340 | * Push it to your ECR repo 341 | * Trigger the Spinnaker pipeline 342 | 343 | Then the Spinnaker pipeline will: 344 | * Start the Spinnaker pipeline 345 | * Parse the artifact with your generated tag 346 | * Replace the `hello-world/nginx` with the reference from your passed-in artifact 347 | * Deploy the hydrated manifest 348 | 349 | ## Put the Kubernetes Manifest in GitHub 350 | 351 | Go into Spinnaker to your pipeline, and grab the Kubernetes manifest 352 | 353 | Go into your GitHub repo (or create a repo), and create the manifest as a file somewhere in the repo (for example, at `/app/manifests/manifest.yml`). 354 | 355 | It should look something like this: 356 | 357 | ```yml 358 | apiVersion: apps/v1 359 | kind: Deployment 360 | metadata: 361 | name: hello-today 362 | spec: 363 | replicas: 3 364 | selector: 365 | matchLabels: 366 | app: hello-today 367 | template: 368 | metadata: 369 | labels: 370 | app: hello-today 371 | lb: hello-today 372 | spec: 373 | containers: 374 | - image: hello-world/nginx 375 | name: primary 376 | ports: 377 | - containerPort: 80 378 | ``` 379 | 380 | * In Spinnaker, go to your "Deploy (Manifest)" stage, and go down to the "Manifest" section. 381 | * Select "Artifact" for the "Manifest Source" 382 | * Select "Define New Artifact". Populate these fields: 383 | * Account: "my-github-credential" (or whatever you specified for the credential name) 384 | * Content URL: `https://api.github.com/repos/$ORG/$REPO/contents/$FILEPATH` (`https://github.mydomain.com/api/v3/repos/$ORG/$REPO/$FILEPATH` for GHE). Replace the org, repo, and filepath with relevant entries for your file. For example: 385 | * GitHub.com: `https://api.github.com/repos/baxterthehacker/public-repo/contents/path/to/file.yml`. 386 | * GHE: `https://github.mydomain.com/api/v3/repos/baxterthehacker/public-repo/contents/path/to/file.yml`) 387 | 388 | Save the pipeline, and re-trigger the Jenkins build. This should do the following: 389 | 390 | Trigger your Jenkins build, and it should do the following: 391 | * Build a new Docker image 392 | * Push it to your ECR repo 393 | * Trigger the Spinnaker pipeline 394 | 395 | Then the Spinnaker pipeline will: 396 | * Start the Spinnaker pipeline 397 | * Parse the artifact with your generated tag 398 | * Pull the Kubernetes manifest from GitHub (Enterprise) 399 | * Replace the `hello-world/nginx` with the reference from your passed-in artifact 400 | * Deploy the hydrated manifest 401 | * Wait for the pods to be fully up 402 | 403 | Verify your deployed image is the most recent tag. 404 | 405 | ## Remove the default artifact 406 | 407 | * Go into the configuration for your pipeline 408 | * Go to your artifact constraint, and click the pencil icon 409 | * Uncheck the default artifact checkbox 410 | 411 | Save the pipeline. 412 | 413 | Do a hard refresh of the page to verify the default artifact is removed (click on the pencil again). 414 | 415 | Save the pipeline, and re-trigger the Jenkins build. This should do the following: 416 | 417 | Trigger your Jenkins build, and it should do the following: 418 | * Build a new Docker image 419 | * Push it to your ECR repo 420 | * Trigger the Spinnaker pipeline 421 | 422 | Then the Spinnaker pipeline will: 423 | * Start the Spinnaker pipeline 424 | * Parse the artifact with your generated tag 425 | * Pull the Kubernetes manifest from GitHub (Enterprise) 426 | * Replace the `hello-world/nginx` with the reference from your passed-in artifact 427 | * Deploy the hydrated manifest 428 | * Wait for the pods to be fully up -------------------------------------------------------------------------------- /guides/setup-dev-environment.md: -------------------------------------------------------------------------------- 1 | # Setup Local Debugging for Spinnaker Services 2 | 3 | ## Minimum System requirements 4 | - Windows or Mac OS X 5 | - 16GB of Memory 6 | - 30GB of Available Storage 7 | 8 | This allows you to do something like this: 9 | 10 | * OSX/Windows workstation, with an Ubuntu VM running in multipass, with everything directly wired up. 11 | * Some services running locally in your workstation (via IntelliJ) 12 | * All other services running in Minnaker (on the VM) 13 | 14 | For example: 15 | * OSX/Windows using IP 192.168.64.1 and the VM using 192.168.64.6 16 | * Orca running on http://192.168.64.1:8083 17 | * All other services running on 192.168.64.6 (for example, Clouddriver will be on http://192.168.64.6:7002) 18 | 19 | # Install Instructions 20 | 21 | ## Mac OS X 22 | 23 | * Install [homebrew](https://brew.sh/) 24 | 25 | ```bash 26 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)" 27 | ``` 28 | 29 | ## Windows or Mac OS X 30 | 31 | * Install a [JDK](https://adoptopenjdk.net/installation.html) 11.0.8 32 | 33 | * Mac OS X 34 | 35 | ```bash 36 | brew tap AdoptOpenJDK/openjdk 37 | brew cask install adoptopenjdk11 38 | ``` 39 | 40 | * Windows [instructions](https://www.oracle.com/java/technologies/javase-jdk11-downloads.html) 41 | 42 | 43 | * Install [Multipass](https://multipass.run/) 44 | 45 | * Mac instructions 46 | ```bash 47 | brew cask install multipass 48 | ``` 49 | * Windows [instructions](https://multipass.run/download/windows) 50 | 51 | * Install [IntelliJ Community Edition](https://www.jetbrains.com/idea/download/) 52 | 53 | * Mac instructions 54 | ```bash 55 | brew cask install intellij-idea-ce 56 | ``` 57 | * Windows [instructions](https://adoptopenjdk.net/installation.html#x64_win-jdk) 58 | 59 | * Install Yarn (installs Node.js if not installed). 60 | * Mac [instructions](https://classic.yarnpkg.com/en/docs/install#mac-stable) 61 | ```bash 62 | brew install yarn 63 | ``` 64 | * Windows [instructions](https://classic.yarnpkg.com/en/docs/install#windows-stable) 65 | 66 | * Install `kubectl`. 67 | * Mac instructions 68 | ```bash 69 | brew install kubectl 70 | ``` 71 | * Windows [instructions](https://kubernetes.io/docs/tasks/tools/install-kubectl/#install-kubectl-on-windows) 72 | 73 | 74 | # Getting Spinnaker Up and Running 75 | 76 | Open two terminals one will be for shell access into minnaker-vm the other will be for host machine. 77 | - Windows or Mac OS X terminal will be referred to as [host] 78 | - minnaker-vm terminal will be referred to as [minnaker-vm] 79 | 80 | ## Install Spinnaker in a Multipass VM 81 | 82 | 1. [minnaker-vm] Start a multipass vm **with 2 cores, 10GB of memory, 30GB of storage** 83 | 84 | ```bash 85 | multipass launch -c 2 -m 10G -d 30G --name minnaker-vm 86 | ``` 87 | 88 | 1. [minnaker-vm] Shell into your multipass vm 89 | 90 | ```bash 91 | multipass shell minnaker-vm 92 | ``` 93 | 94 | 1. [minnaker-vm] Download and install Minnaker (use open source, no-auth mode) 95 | 96 | ```bash 97 | curl -LO https://github.com/armory/minnaker/releases/latest/download/minnaker.tgz 98 | tar -xzvf minnaker.tgz 99 | ./minnaker/scripts/no_auth_install.sh -o 100 | ``` 101 | 102 | 1. [minnaker-vm] When it's done, you'll get the IP address of Minnaker. Remember this (or you can always just run `cat /etc/spinnaker/.hal/public_endpoint`) 103 | 104 | *(if you accidentally forget to use no auth or open source, you can run `./minnaker/scripts/utils/remove_auth.sh` and `./minnaker/scripts/utils/switch_to_oss.sh`)* 105 | 106 | ## Prepare Host machine to connect to the Minnaker-VM 107 | 108 | 1. [minnaker-vm] Run this script to ensure each Spinnaker service gets a K8s LoadBalancer and can be accessed from your host machine. 109 | 110 | ```bash 111 | ./minnaker/scripts/utils/expose_local.sh 112 | ``` 113 | 114 | 6. [minnaker-vm] Check on the status of spinnaker 115 | 116 | ``` 117 | kubectl get pods -n spinnaker 118 | ``` 119 | All pods need to show `1/1` for `READY`. 120 | 121 | 7. [host] You can now browse to spinnaker at https://192.168.64.6 122 | - Troubleshooting: 123 | - `Service Unavailable`: wait until spinnaker starts up, it can take a while to start up (download all docker images) the above step will show you if it is up and running. 124 | 125 | 8. [minnaker-vm] Expose the service you want to debug (example here is orca) 126 | ```bash 127 | ./minnaker/scripts/utils/external_service_setup.sh orca 128 | ``` 129 | 130 | You can also expose multiple services 131 | ```bash 132 | ./minnaker/scripts/utils/external_service_setup.sh orca echo 133 | ``` 134 | 135 | 9. [host] Setup your host config files 136 | - Create/edit the file `~/.spinnaker/spinnaker-local.yml`, and paste the previously copied output. 137 | ``` 138 | services: 139 | front50: 140 | baseUrl: http://192.168.64.6:8080 141 | redis: 142 | baseUrl: http://192.168.64.6:6379 143 | clouddriver: 144 | baseUrl: http://192.168.64.6:7002 145 | orca: 146 | host: 0.0.0.0 147 | echo: 148 | baseUrl: http://192.168.64.6:8089 149 | deck: 150 | baseUrl: http://192.168.64.6:9000 151 | rosco: 152 | baseUrl: http://192.168.64.6:8087 153 | gate: 154 | baseUrl: http://192.168.64.6:8084 155 | ``` 156 | - Create/edit the config file for the service you are going to debug (example orca). 157 | - [minnaker-vm] 158 | ```bash 159 | cat /etc/spinnaker/.hal/default/staging/orca.yml 160 | ``` 161 | - [host] create a `~/.spinnaker/orca.yml` file with the above files contents. 162 | 163 | 10. Choose a working directory, and go there. I usually use `~/git/spinnaker` 164 | 165 | ```bash 166 | mkdir -p ~/git/spinnaker 167 | cd ~/git/spinnaker 168 | ``` 169 | 170 | 11. Clone the service you want 171 | 172 | ```bash 173 | git clone https://github.com/spinnaker/orca.git 174 | ``` 175 | 176 | _or, if you have a Git SSH key set up_ 177 | 178 | ```bash 179 | git clone git@github.com:spinnaker/orca.git 180 | ``` 181 | 182 | 12. Change the branch 183 | 184 | ```bash 185 | cd orca 186 | git branch -a 187 | ``` 188 | 189 | You'll see a list of branches (like `remotes/origin/release-1.22.x`). The last bit (after the last slash) is the branch name. Check out that branch. 190 | 191 | ```bash 192 | git checkout release-1.22.x 193 | ``` 194 | 195 | 13. Open IntelliJ 196 | 197 | 14. Open your project 198 | 199 | * If you don't have a project open, you'll see a "Welcome to IntellJ IDEA". 200 | 201 | 1. Click "Open or Import" 202 | 203 | 2. Navigate to your directory (e.g., `~/git/spinnaker/orca`) 204 | 205 | 3. Click on `build.gradle` and click "Open" 206 | 207 | 4. Select "Open as Project" 208 | 209 | * If you already have one or more projects open, do the following: 210 | 211 | 1. Use the menu "File" > "Open" 212 | 213 | 2. Navigate to your directory (e.g., `~/git/spinnaker/orca`) 214 | 215 | 3. Click on `build.gradle` and click "Open" 216 | 217 | 4. Select "Open as Project" 218 | 219 | 15. Wait for the thing to do the thing. It's gotta load the stuff. 220 | 221 | 16. Through the next few steps, if you hit an "Unable to find Main" or fields are grayed out, reimport the project: 222 | 223 | 1. View > Tool Windows > Gradle 224 | 225 | 2. In the Gradle window, right click "Orca" and then click "Reimport Gradle Project" 226 | 227 | 17. In the top right corner of the project window, there's a "Add Configuration" button. Click it. 228 | 229 | 18. Click the little '+' sign in the top left corner, and select "Application" 230 | 231 | 19. Give it a name. Like "Main" or "Run Orca" 232 | 233 | 20. Click the three dots next to "Main Class". Either wait for it to load and select "Main (com.netflix.spinnaker.orca) or click on "Project" and navigate to `orca > orca-web > src > main > groovy > com.netflix.spinnaker > orca > Main` 234 | 235 | 21. In the dropdown for "Use classpath of module", select "orca-web_main" 236 | 237 | 22. Click "Apply" and then "OK" 238 | 239 | 23. To build and run the thing, click the little green triangle next to your configuration (top right corner, kinda) 240 | 241 | Now magic happens. 242 | 243 | ## Some Cleanup Commands for later 244 | 245 | ### How to reset your minnaker-vm 246 | 247 | [minnaker-vm] Run the following to no longer debug from host 248 | 249 | ```bash 250 | ./minnaker/scripts/utils/external_service_setup.sh 251 | ``` 252 | 253 | ### How to stop spinnaker 254 | 255 | [host] Run the following to stop the minnaker-vm (spinnaker) 256 | 257 | ```bash 258 | multipass stop minnaker-vm 259 | ``` 260 | 261 | ## [Optional] Setup kubectl on host 262 | 263 | 1. [minnaker-vm] Get your kubernetes config file 264 | 265 | ```bash 266 | kubectl config view --raw 267 | ``` 268 | 269 | Example Output: 270 | ``` 271 | apiVersion: v1 272 | clusters: 273 | - cluster: 274 | certificate-authority-data: YOUR_CERT_HERE 275 | server: https://127.0.0.1:6443 276 | name: default 277 | contexts: 278 | - context: 279 | cluster: default 280 | namespace: spinnaker 281 | user: default 282 | name: default 283 | current-context: default 284 | kind: Config 285 | preferences: {} 286 | users: 287 | - name: default 288 | user: 289 | password: YOUR_PASSWORD_HERE 290 | username: admin 291 | ``` 292 | 293 | 1. [host] Save the command output from above command `kubectl config view --raw` to `~/.kube/minnaker` on host machine 294 | 295 | 1. [minnaker-vm] To get the IP of minnaker-vm 296 | 297 | ```bash 298 | cat /etc/spinnaker/.hal/public_endpoint 299 | ``` 300 | 301 | 1. [host] Edit `~/.kube/minnaker` to have the IP address of the minnaker-vm 302 | New File: 303 | ``` 304 | apiVersion: v1 305 | clusters: 306 | - cluster: 307 | certificate-authority-data: YOUR_CERT_HERE 308 | server: https://192.168.64.6:6443 309 | name: default 310 | contexts: 311 | - context: 312 | cluster: default 313 | namespace: spinnaker 314 | user: default 315 | name: default 316 | current-context: default 317 | kind: Config 318 | preferences: {} 319 | users: 320 | - name: default 321 | user: 322 | password: YOUR_PASSWORD_HERE 323 | username: admin 324 | ``` 325 | 326 | 1. [host] Setup `kubectl` from HOST to check on the deploy 327 | 328 | ``` 329 | export KUBECONFIG=~/.kube/minnaker 330 | kubectl get pods -n spinnaker 331 | 332 | ``` 333 | or always specify `--kubeconfig ~/.kube/minnaker` 334 | ``` 335 | kubectl --kubeconfig ~/.kube/minnaker get pods -n spinnaker 336 | ``` 337 | 338 | 2. [host] Now you can run local kubectl command 339 | ```bash 340 | kubectl get pods -n spinnaker 341 | ``` 342 | 343 | ## Start doing plugin-ey things 344 | 345 | Follow the "debugging" section here: https://github.com/spinnaker-plugin-examples/pf4jStagePlugin 346 | 347 | notes: 348 | * Create the `plugins` directory in the git repo (e.g., `~/git/spinnaker/orca/plugins`) and put the `.plugin-ref` in there 349 | * If you don't see the gradle tab, you can get to it with View > Tool Windows > Gradle 350 | 351 | ## Build and test the randomWait stage 352 | 353 | This assumes you have a Github account, and are logged in. 354 | 355 | 1. You *probably* want to work on a fork. Go to github.com/spinnaker-plugin-examples/pf4jStagePlugin 356 | 357 | 1. In the top right corner, click "Fork" and choose your username to create a fork. For example, mine is `justinrlee` so I end up with github.com/justinrlee/pf4jStagePlugin 358 | 359 | 1. On your workstation, choose a working directory. For example, `~/git/justinrlee` 360 | 361 | ```bash 362 | mkdir -p ~/git/justinrlee 363 | cd ~/git/justinrlee 364 | ``` 365 | 366 | 1. Clone the repo 367 | 368 | ```bash 369 | git clone https://github.com/justinrlee/pf4jStagePlugin.git 370 | ``` 371 | 372 | _or, if you have a Git SSH key set up_ 373 | 374 | ```bash 375 | git clone git@github.com:justinrlee/pf4jStagePlugin.git 376 | ``` 377 | 378 | 1. Check out a tag. 379 | 380 | If you are using Spinnaker 1.19.x, you probably need a 1.0.x tag (1.0.x is compatible 1.19, 1.1.x is compatible with 1.20) 381 | 382 | List available tags: 383 | 384 | ```bash 385 | cd pf4jStagePlugin 386 | git tag -l 387 | ``` 388 | 389 | Check out the tag you want: 390 | 391 | ```bash 392 | git checkout v1.0.17 393 | ``` 394 | 395 | Create a branch off of it (optional, but good if you're gonna be making changes). This creates a branch called custom-stage 396 | 397 | ```bash 398 | git switch -c custom-stage 399 | ``` 400 | 401 | 1. Build the thing from the CLI 402 | 403 | ```bash 404 | ./gradlew releaseBundle 405 | ``` 406 | 407 | This will generate an orca .plugin-ref file (`random-wait-orca/build/orca.plugin-ref`). 408 | 409 | 1. Copy the `orca.plugin-ref` file to the `plugins` directory in your `orca` repo. 410 | 411 | Create the destination directory - this will depend on where you cloned the orca repo 412 | 413 | ```bash 414 | mkdir -p ~/git/spinnaker/orca/plugins 415 | ``` 416 | 417 | Copy the file 418 | 419 | ```bash 420 | cp random-wait-orca/build/orca.plugin-ref ~/git/spinnaker/orca/plugins/ 421 | ``` 422 | 423 | 1. Create the orca-local.yml file in `~/.spinnaker/` 424 | 425 | This tells Spinnaker to enable and use the plugin 426 | 427 | Create this file at `~/.spinnaker/orca-local.yml`: 428 | 429 | ```bash 430 | # ~/.spinnaker/orca-local.yml 431 | spinnaker: 432 | extensibility: 433 | plugins: 434 | Armory.RandomWaitPlugin: 435 | enabled: true 436 | version: 1.0.17 437 | extensions: 438 | armory.randomWaitStage: 439 | enabled: true 440 | config: 441 | defaultMaxWaitTime: 60 442 | ``` 443 | 444 | 1. In IntelliJ (where you have the Orca project open), Link the plugin project to your current project 445 | 446 | 1. Open the Gradle window if it's not already open (View > Tool Windows > Gradle) 447 | 448 | 1. In the Gradle window, click the little '+' sign 449 | 450 | 1. Navigate to your plugin directory (e.g., `/git/justinrlee/pf4jStagePlugin`), and select `build.gradle` and click Open 451 | 452 | 1. In the Gradle window, right click "orca" and click "Reimport Gralde Project" 453 | 454 | 1. In IntelliJ, create a new build configuration 455 | 456 | 1. In the top right, next to the little hammer icon, there's a dropdown. Click "Edit Configurations..." 457 | 458 | 1. Click the '+' sign in the top left, and select "Application" 459 | 460 | 1. Call it something cool. Like "Build and Test Plugin" 461 | 462 | 1. Select the main class (Either wait for it to load and select "Main (com.netflix.spinnaker.orca) or click on "Project" and navigate to `orca > orca-web > src > main > groovy > com.netflix.spinnaker > orca > Main`) 463 | 464 | 1. In the dropdown for "Use classpath of module", select "orca-web_main" 465 | 466 | 1. Put this in the "VM Options" field put this: '`-Dpf4j.mode=development`' 467 | 468 | 1. In the "Before launch" section of the window, click the '+' sign and add "Build Project" 469 | 470 | 1. Select "Build" in the "Before launch" section and click the '-' sign to remove it (you don't need both "Build" and "Build Project") 471 | 472 | 1. Click "Apply" and then "OK" 473 | 474 | 1. Run your stuff. 475 | 476 | 1. If the unmodified Orca is still running, click the little stop icon (red square in top right corner) 477 | 478 | 1. Select your new build configuration in the dropdown 479 | 480 | 1. Click the runicon (little green triangle) 481 | 482 | 1. In the console output you should see something that looks like this: 483 | 484 | ``` 485 | 2020-04-30 10:17:41.242 INFO 53937 --- [ main] com.netflix.spinnaker.orca.Main : [] Starting Main on justin-mbp-16.lan with PID 53937 (/Users/justin/dev/spinnaker/orca/orca-web/build/classes/groovy/main started by justin in /Users/justin/dev/spinnaker/orca) 486 | 2020-04-30 10:17:41.245 INFO 53937 --- [ main] com.netflix.spinnaker.orca.Main : [] The following profiles are active: test,local 487 | 488 | ... 489 | 490 | 2020-04-30 10:17:44.276 WARN 53937 --- [ main] c.n.s.config.PluginsAutoConfiguration : [] No remote repositories defined, will fallback to looking for a 'repositories.json' file next to the application executable 491 | 2020-04-30 10:17:44.410 INFO 53937 --- [ main] org.pf4j.AbstractPluginManager : [] Plugin 'Armory.RandomWaitPlugin@unspecified' resolved 492 | 2020-04-30 10:17:44.411 INFO 53937 --- [ main] org.pf4j.AbstractPluginManager : [] Start plugin 'Armory.RandomWaitPlugin@unspecified' 493 | 2020-04-30 10:17:44.413 INFO 53937 --- [ main] i.a.p.s.wait.random.RandomWaitPlugin : [] RandomWaitPlugin.start() 494 | ``` 495 | 496 | 1. If you see "no class Main.main" or something, in the Gradle window, try right click on "orca" and reimport Gradle project and try again. 497 | 498 | 1. Test your stuff 499 | 500 | 1. Go into the Spinnaker UI (should be http://your-VM-ip:9000) 501 | 502 | 1. Go to applications > spin > pipelines 503 | 504 | 1. Create a new pipeline 505 | 506 | 1. Add stage 507 | 508 | 1. Edit stage as JSON (bottom right) 509 | 510 | 1. Paste this in there: 511 | 512 | ```json 513 | { 514 | "maxWaitTime": 15, 515 | "name": "Test RandomWait", 516 | "type": "randomWait" 517 | } 518 | ``` 519 | 520 | 1. Update stage 521 | 522 | 1. Save changes 523 | 524 | 1. Click back to pipelines (pipelines tab at top) 525 | 526 | Magic. Maybe. Maybe not. 527 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Spinnaker All-In-One (Minnaker) Quick Start 2 | 3 | Minnaker is a simple way to install Spinnaker inside a VM. 4 | 5 | ## Background 6 | 7 | Minnaker performs the following actions when run on a single Linux instance: 8 | 9 | * Installs [k3s](https://k3s.io/) with Traefik. 10 | * Installs minio in k3s with a local volume. 11 | * Installs mysql in k3s. 12 | * Installs redis in k3s. 13 | * Installs **[Spinnaker Operator](https://github.com/armory/spinnaker-operator)**. 14 | * Clones the "minnaker" branch in https://github.com/armory/spinnaker-kustomize-patches for the purposes of configuring Spinnaker. 15 | * Installs and configures **[Spinnaker](https://github.com/spinnaker)** or **[Armory](https://armory.io)** using the **Spinnaker Operator**. 16 | * Exposes Spinnaker using an Ingress. NOTE: If you're using an AWS EC2 instance, make sure you add port 443 to the security group. 17 | * Minnaker uses local authentication. The username is `admin` and the password is randomly generated when you install Minnaker. Find more details about getting the password in [Accessing Spinnaker](#accessing-spinnaker). 18 | * For the full list of customizations and configurations - please check out the [kustomization-minnaker.yml] (https://github.com/armory/spinnaker-kustomize-patches/blob/minnaker/recipes/kustomization-minnaker.yml) file. 19 | 20 | ## Requirements 21 | 22 | To use Minnaker, make sure your Linux instance meets the following requirements: 23 | 24 | * Linux distribution running in a VM or bare metal 25 | * Ubuntu 18.04 or Debian 10 (VM or bare metal) 26 | * 2 vCPUs (recommend 4) 27 | * 8GiB of RAM (recommend 16) 28 | * 30GiB of HDD (recommend 40+) 29 | * NAT or Bridged networking with access to the internet 30 | * Install `curl`, `git`, and `tar` (if they're not already installed): 31 | * `sudo apt-get install curl git tar` 32 | * Port `443` on your VM needs to be accessible from your workstation / browser. By default, Minnaker installs Spinnaker and configures it to listen on port `443`, using paths `/` and `/api/v1`(for the UI and API). 33 | * OSX 34 | * Docker Desktop local Kubernetes cluster enabled 35 | * At least 6 GiB of memory allocated to Docker Desktop 36 | 37 | * On Ubuntu, the Minnaker installer will install K3s for you (a minimal installation of Kubernetes), so you do not have to pre-install Docker or Kubernetes. 38 | 39 | ## Changelog 40 | 41 | * 2/XX/2021 - Major update - install.sh has been replaced to use the spinnaker operator as the default installation method. Todo: Many of the convience scripts will also need to be updated to use the operator as well. If you would still like to use Halyard - please reference [Release 0.0.23](https://github.com/armory/minnaker/releases/tag/0.0.22) 42 | * operator_install.sh replaces install.sh 43 | * removing operator_install.sh 44 | * ToDo: Clean up all other scripts to remove dependency on halyard. 45 | * see notes below on currently supported scripts 46 | 47 | --- 48 | 49 | ## Installation 50 | 51 | 1. Login (SSH) to your VM or bare metal box. 52 | 2. Download the minnaker tarball and untar: 53 | 54 | ```bash 55 | curl -L https://github.com/armory/minnaker/archive/v0.1.3.tar.gz | tar -zxv 56 | ``` 57 | 58 | 3. Change into the directory: 59 | 60 | ```bash 61 | cd minnaker-0.1.* 62 | ``` 63 | 64 | 4. Execute the install script. Note the following options before running the script: 65 | * Add the `-o` flag if you want to install open source Spinnaker. 66 | * By default, the script installs Armory Spinnaker and uses your public IP address (determined by `curl`ing `ifconfig.co`) as the endpoint for Spinnaker. 67 | * For bare metal or a local VM, specify the IP address for your server with `-P` flag. `-P` is the 'Public Endpoint' and must be an address or DNS name you will use to access Spinnaker (an IP address reachable by your end users). 68 | 69 | ```bash 70 | ./scripts/install.sh 71 | ``` 72 | 73 | For example, the following command installs OSS Spinnaker on a VM with the IP address of `192.168.10.1`: 74 | 75 | ```bash 76 | export PRIVATE_IP=192.168.10.1 77 | ./scripts/install.sh -o -P $PRIVATE_IP 78 | ``` 79 | 80 | Installation can take between 5-10 minutes to complete depending on VM size. 81 | 82 | 5. Once Minnaker is up and running, you can make changes to its configuration using `kustomize` and the `spinnaker-operator` under the folder `~/minnaker-1.0.1/spinsvc`. For example, to change the version of Spinnaker that is installed, you can do this: 83 | 84 | * Using your favorite editor, edit the file: `~/minnaker-1.0.1/spinsvc/core_config/patch-version.yml` 85 | * Update line 8 to the version you desire. e.g. `version: 2.24.0` 86 | * Then either run `cd ~/minnaker-1.0.1/spinsvc && ./deploy.sh` or `kubectl apply -k ~/minnaker-1.0.1/spinsvc` 87 | * To find the latest versions available: 88 | * [Spinnaker](https://spinnaker.io/community/releases/versions/#latest-stable) 89 | * [Armory](https://docs.armory.io/docs/release-notes/rn-armory-spinnaker/) 90 | * *By default, Minnaker will install the latest GA version of Spinnaker or Armory available.* 91 | 92 | ## Accessing Spinnaker 93 | 94 | 1. A helper script called `spin_endpoint` was created during the installation process that prints out the URL associated with your spinnaker instance as well as the credentials (as necessary). 95 | 96 | ```bash 97 | spin_endpoint 98 | ``` 99 | 100 | outputs: 101 | ```bash 102 | https://192.168.64.3 103 | username: 'admin' 104 | password: 'xxxxx' 105 | ``` 106 | 107 | 2. In your browser, navigate to the address (https://192.168.64.3/) for Spinnaker from step 1. This is Deck, the Spinnaker UI. 108 | 109 | If you installed Minnaker on a local VM, you must access it from your local machine. If you deployed Minnaker in the cloud, such as an EC2 instance, you can access Spinnaker from any machine that has access to that 'Public IP'. 110 | 111 | 3. Log in to Deck with the following credentials: 112 | 113 | Username: `admin` 114 | 115 | Password: