├── .circleci └── config.yml ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yaml │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── chart-release.yml │ ├── ci.yaml │ ├── codeql-analysis.yml │ ├── fluxctl-snap.yaml │ ├── fossa.yml │ ├── markdown-link-check-config.json │ └── rebase.yaml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DCO ├── LICENSE ├── MAINTAINERS ├── Makefile ├── README.md ├── bin ├── kubeyaml └── upload-binaries ├── chart ├── README.md └── flux │ ├── .helmignore │ ├── CHANGELOG.md │ ├── Chart.yaml │ ├── README.md │ ├── dashboards │ └── flux.json │ ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── configmap-dashboards.yaml │ ├── deployment.yaml │ ├── gitconfig.yaml │ ├── kube.yaml │ ├── memcached.yaml │ ├── psp.yaml │ ├── rbac-role.yaml │ ├── rbac.yaml │ ├── secret.yaml │ ├── service.yaml │ ├── serviceaccount.yaml │ ├── servicemonitor.yaml │ └── ssh.yaml │ └── values.yaml ├── cmd ├── fluxctl │ ├── args.go │ ├── args_test.go │ ├── automate_cmd.go │ ├── await.go │ ├── completion_cmd.go │ ├── completion_cmd_test.go │ ├── deautomate_cmd.go │ ├── error.go │ ├── format.go │ ├── identity_cmd.go │ ├── install_cmd.go │ ├── install_cmd_test.go │ ├── list_images_cmd.go │ ├── list_images_cmd_test.go │ ├── list_workloads_cmd.go │ ├── list_workloads_cmd_test.go │ ├── lock_cmd.go │ ├── main.go │ ├── main_test.go │ ├── policy_cmd.go │ ├── portforward.go │ ├── release_cmd.go │ ├── release_cmd_test.go │ ├── root_cmd.go │ ├── save_cmd.go │ ├── sync_cmd.go │ ├── unlock_cmd.go │ ├── version_cmd.go │ └── version_cmd_test.go └── fluxd │ └── main.go ├── deploy ├── flux-account.yaml ├── flux-deployment.yaml ├── flux-ns.yaml ├── flux-secret.yaml ├── kustomization.yaml ├── memcache-dep.yaml └── memcache-svc.yaml ├── docker ├── Dockerfile.flux ├── Dockerfile.fluxctl ├── image-tag ├── known_hosts.sh ├── kubeconfig ├── kubectl.version ├── kustomize.version ├── sops.version └── ssh_config ├── docs ├── README.md ├── _files │ ├── flux-cd-diagram.png │ └── weave-flux.png ├── contributing │ ├── building.md │ └── get-started-developing.md ├── faq.md ├── get-started.md ├── guides │ ├── provide-own-ssh-key.md │ ├── upgrading-to-1.0.md │ ├── use-git-https.md │ ├── use-gke-workload-identity.md │ └── use-private-git-host.md ├── introduction.md ├── references │ ├── automated-image-update.md │ ├── blueprint.md │ ├── daemon.md │ ├── fluxctl.md │ ├── fluxyaml-config-files.md │ ├── garbagecollection.md │ ├── git-gpg.md │ ├── helm-operator-integration.md │ └── monitoring.md ├── requirements.md ├── troubleshooting.md └── tutorials │ ├── driving-flux.md │ ├── get-started-helm.md │ ├── get-started-kustomize.md │ └── get-started.md ├── go.mod ├── go.sum ├── internal ├── cmd │ └── changelog │ │ └── main.go └── docs │ └── releasing.md ├── pkg ├── api │ ├── api.go │ ├── v10 │ │ └── api.go │ ├── v11 │ │ └── api.go │ ├── v6 │ │ ├── api.go │ │ ├── container.go │ │ └── container_test.go │ └── v9 │ │ ├── api.go │ │ ├── change.go │ │ └── change_test.go ├── checkpoint │ ├── checkpoint.go │ ├── types.go │ └── util.go ├── cluster │ ├── cluster.go │ ├── includelist.go │ ├── includelist_test.go │ ├── kubernetes │ │ ├── cached_disco.go │ │ ├── cached_disco_test.go │ │ ├── doc.go │ │ ├── errors.go │ │ ├── images.go │ │ ├── images_test.go │ │ ├── kubernetes.go │ │ ├── kubernetes_test.go │ │ ├── kubeyaml.go │ │ ├── manifests.go │ │ ├── manifests_test.go │ │ ├── mock.go │ │ ├── namespacer.go │ │ ├── namespacer_test.go │ │ ├── patch.go │ │ ├── patch_test.go │ │ ├── policies.go │ │ ├── policies_test.go │ │ ├── resource │ │ │ ├── cronjob.go │ │ │ ├── daemonset.go │ │ │ ├── deployment.go │ │ │ ├── doc.go │ │ │ ├── helmrelease.go │ │ │ ├── helmrelease_test.go │ │ │ ├── list.go │ │ │ ├── load.go │ │ │ ├── load_test.go │ │ │ ├── namespace.go │ │ │ ├── resource.go │ │ │ ├── spec.go │ │ │ └── statefulset.go │ │ ├── resourcekinds.go │ │ ├── sshkeyring.go │ │ ├── sync.go │ │ ├── sync_test.go │ │ ├── testfiles │ │ │ ├── data.go │ │ │ └── data_test.go │ │ ├── update.go │ │ └── update_test.go │ ├── mock │ │ └── mock.go │ └── sync.go ├── daemon │ ├── daemon.go │ ├── daemon_test.go │ ├── errors.go │ ├── images.go │ ├── images_test.go │ ├── loop.go │ ├── metrics.go │ ├── note.go │ ├── sync.go │ └── sync_test.go ├── errors │ ├── errors.go │ └── errors_test.go ├── event │ ├── event.go │ └── event_test.go ├── git │ ├── errors.go │ ├── export.go │ ├── export_test.go │ ├── gittest │ │ ├── repo.go │ │ └── repo_test.go │ ├── metrics.go │ ├── mirrors.go │ ├── operations.go │ ├── operations_test.go │ ├── repo.go │ ├── signature.go │ ├── url.go │ ├── url_test.go │ └── working.go ├── gpg │ ├── gpg.go │ └── gpgtest │ │ └── gpg.go ├── guid │ └── guid.go ├── http │ ├── accept.go │ ├── accept_test.go │ ├── client │ │ └── client.go │ ├── daemon │ │ ├── server.go │ │ ├── server_test.go │ │ ├── upstream.go │ │ └── upstream_test.go │ ├── errors.go │ ├── httperror │ │ └── api_error.go │ ├── routes.go │ ├── transport.go │ ├── validate.go │ └── websocket │ │ ├── client.go │ │ ├── ping.go │ │ ├── server.go │ │ ├── websocket.go │ │ └── websocket_test.go ├── image │ ├── image.go │ └── image_test.go ├── install │ ├── generate.go │ ├── generated_templates.gogen.go │ ├── go.mod │ ├── go.sum │ ├── install.go │ ├── install_test.go │ └── templates │ │ ├── flux-account.yaml.tmpl │ │ ├── flux-deployment.yaml.tmpl │ │ ├── flux-secret.yaml.tmpl │ │ ├── memcache-dep.yaml.tmpl │ │ └── memcache-svc.yaml.tmpl ├── job │ ├── job.go │ ├── job_test.go │ └── status_cache.go ├── manifests │ ├── configaware.go │ ├── configaware_test.go │ ├── configfile.go │ ├── configfile_test.go │ ├── manifests.go │ ├── rawfiles.go │ └── store.go ├── metrics │ └── metrics.go ├── policy │ ├── pattern.go │ ├── pattern_test.go │ ├── policy.go │ └── policy_test.go ├── portforward │ ├── portforward.go │ └── portforward_test.go ├── registry │ ├── aws.go │ ├── azure.go │ ├── azure_test.go │ ├── cache │ │ ├── cache.go │ │ ├── doc.go │ │ ├── memcached │ │ │ ├── integration_test.go │ │ │ ├── memcached.go │ │ │ └── memcached_test.go │ │ ├── monitoring.go │ │ ├── registry.go │ │ ├── registry_test.go │ │ ├── repocachemanager.go │ │ ├── repocachemanager_test.go │ │ ├── warming.go │ │ └── warming_test.go │ ├── client.go │ ├── client_factory.go │ ├── credentials.go │ ├── credentials_test.go │ ├── doc.go │ ├── gcp.go │ ├── imageentry_test.go │ ├── middleware │ │ └── rate_limiter.go │ ├── mock │ │ └── mock.go │ ├── monitoring.go │ └── registry.go ├── release │ ├── context.go │ ├── errors.go │ ├── releaser.go │ └── releaser_test.go ├── remote │ ├── doc.go │ ├── errors.go │ ├── logging.go │ ├── metrics.go │ ├── mock.go │ ├── mock_test.go │ └── rpc │ │ ├── baseclient.go │ │ ├── clientV10.go │ │ ├── clientV11.go │ │ ├── clientV6.go │ │ ├── clientV7.go │ │ ├── clientV8.go │ │ ├── clientV9.go │ │ ├── compat.go │ │ ├── doc.go │ │ ├── rpc_test.go │ │ └── server.go ├── resource │ ├── id.go │ ├── id_test.go │ ├── policy.go │ └── resource.go ├── ssh │ ├── keygen.go │ └── keyring.go ├── sync │ ├── git.go │ ├── mock.go │ ├── provider.go │ ├── secret.go │ ├── sync.go │ └── sync_test.go └── update │ ├── automated.go │ ├── automated_test.go │ ├── filter.go │ ├── images.go │ ├── images_test.go │ ├── menu.go │ ├── menu_unix.go │ ├── menu_win.go │ ├── metrics.go │ ├── print.go │ ├── print_test.go │ ├── release_containers.go │ ├── release_image.go │ ├── result.go │ ├── spec.go │ ├── spec_test.go │ ├── sync.go │ └── workload.go ├── snap └── snapcraft.yaml ├── test └── e2e │ ├── 10_helm_chart.bats │ ├── 11_fluxctl_install.bats │ ├── 12_sync.bats │ ├── 13_sync_gc.bats │ ├── 14_release_image.bats │ ├── 15_fluxctl_list.bats │ ├── 16_fluxctl_sync.bats │ ├── 17_fluxctl_policies.bats │ ├── 20_commit_signing.bats │ ├── 20_commit_verification.bats │ ├── 21_ssh_key_generation.bats │ ├── 22_manifest_generation.bats │ ├── fixtures │ ├── crane_empty_img_tmpl │ │ ├── config.json │ │ └── manifest.json │ ├── gitconfig │ └── kustom │ │ ├── 13_sync_gc │ │ ├── gc_patch.yaml │ │ └── kustomization.yaml │ │ ├── 14_release_image │ │ ├── kustomization.yaml │ │ └── release_image_patch.yaml │ │ ├── 15_fluxctl_sync │ │ ├── fluxctl_sync.yaml │ │ └── kustomization.yaml │ │ ├── 20_gpg │ │ ├── flux │ │ │ ├── gpg_patch.yaml │ │ │ └── kustomization.yaml │ │ └── gitsrv │ │ │ ├── gpg_patch.yaml │ │ │ └── kustomization.yaml │ │ ├── 22_manifest_generation │ │ ├── flux │ │ │ ├── kustomization.yaml │ │ │ └── manifest_generation_patch.yaml │ │ └── gitsrv │ │ │ ├── kustomization.yaml │ │ │ └── manifest_generation_patch.yaml │ │ └── base │ │ ├── flux │ │ ├── e2e_patch.yaml │ │ └── kustomization.yaml │ │ ├── gitsrv │ │ ├── gitsrv.yaml │ │ └── kustomization.yaml │ │ └── registry │ │ ├── kustomization.yaml │ │ └── registry.yaml │ ├── lib │ ├── defer.bash │ ├── env.bash │ ├── gpg.bash │ ├── install.bash │ ├── poll.bash │ ├── registry.bash │ └── template.bash │ ├── run-gh.bash │ └── run.bash └── tools.go /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 51 | -------------------------------------------------------------------------------- /.github/workflows/chart-release.yml: -------------------------------------------------------------------------------- 1 | name: release-chart 2 | on: 3 | push: 4 | tags: 'chart-*' 5 | 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Publish Helm chart 12 | uses: stefanprodan/helm-gh-pages@master 13 | with: 14 | token: ${{ secrets.BOT_GITHUB_TOKEN }} 15 | charts_dir: chart 16 | charts_url: https://charts.fluxcd.io 17 | owner: fluxcd 18 | repository: charts 19 | branch: gh-pages 20 | linting: off 21 | -------------------------------------------------------------------------------- /.github/workflows/fluxctl-snap.yaml: -------------------------------------------------------------------------------- 1 | name: Build snap 2 | 3 | on: 4 | push: 5 | branches: 6 | - disabled 7 | 8 | jobs: 9 | build-snap: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Check out Git repository 14 | uses: actions/checkout@v2 15 | 16 | - name: Install Snapcraft 17 | uses: samuelmeuli/action-snapcraft@v1 18 | with: 19 | snapcraft_token: ${{ secrets.snapcraft_token }} 20 | use_lxd: true 21 | 22 | - name: Run Snapcraft build 23 | run: sg lxd -c 'snapcraft --use-lxd' 24 | 25 | - name: Upload to Snapcraft store (edge channel for revs in master) 26 | if: ${{ ! startsWith(github.event.ref, 'refs/tags') }} 27 | run: snapcraft push --release edge fluxctl_*.snap 28 | 29 | - name: Upload to Snapcraft store (stable channel for tagged releases) 30 | if: ${{ startsWith(github.event.ref, 'refs/tags') }} 31 | run: snapcraft push --release stable fluxctl_*.snap 32 | -------------------------------------------------------------------------------- /.github/workflows/fossa.yml: -------------------------------------------------------------------------------- 1 | name: FOSSA 2 | on: 3 | push: 4 | branches: [master] 5 | pull_request: 6 | branches: [master] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-go@v2 14 | with: 15 | go-version: "^1.16.x" 16 | - name: Add GOPATH to GITHUB_ENV 17 | run: echo "GOPATH=$(go env GOPATH)" >>"$GITHUB_ENV" 18 | - name: Add GOPATH to GITHUB_PATH 19 | run: echo "$GOPATH/bin" >>"$GITHUB_PATH" 20 | - name: Run FOSSA scan and upload build data 21 | uses: fossa-contrib/fossa-action@v1 22 | with: 23 | # FOSSA Push-Only API Token 24 | fossa-api-key: 5ee8bf422db1471e0bcf2bcb289185de 25 | github-token: ${{ github.token }} 26 | -------------------------------------------------------------------------------- /.github/workflows/markdown-link-check-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignorePatterns": [ 3 | { "pattern": "^https://github.com/\\S+/\\S+/(issues|pull)/[0-9]+" }, 4 | { "pattern": "^mailto:" }, 5 | { "pattern": "^https://www.(blablacar|canva|underarmour).com" }, 6 | { "pattern": "https://rakuten.com" } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.github/workflows/rebase.yaml: -------------------------------------------------------------------------------- 1 | name: rebase 2 | 3 | on: 4 | pull_request: 5 | types: [opened] 6 | issue_comment: 7 | types: [created] 8 | 9 | jobs: 10 | rebase: 11 | if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/rebase') && (github.event.comment.author_association == 'CONTRIBUTOR' || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout the latest code 15 | uses: actions/checkout@v2 16 | with: 17 | fetch-depth: 0 18 | - name: Automatic Rebase 19 | uses: cirrus-actions/rebase@1.3.1 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | # Tool related configurations 27 | .ackrc 28 | .envrc 29 | 30 | ### Package builds (e.g. snap of fluxctl) 31 | fluxctl_*_*.snap 32 | fluxctl_*_*.snap.xdelta3 33 | parts 34 | prime 35 | stage 36 | 37 | 38 | # Specific to this project 39 | vendor/* 40 | !vendor/manifest 41 | build/ 42 | /cache/* 43 | testdata/helloworld/helloworld-linux-amd64 44 | testdata/helloworld/.helloworld 45 | testdata/sidecar/sidecar-linux-amd64 46 | testdata/sidecar/.sidecar 47 | docker/fluxy-dumbconf.priv 48 | test/profiles 49 | test/bin/ 50 | test/e2e/bats 51 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Flux Community Code of Conduct 2 | 3 | Flux follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). 4 | -------------------------------------------------------------------------------- /DCO: -------------------------------------------------------------------------------- 1 | Developer Certificate of Origin 2 | Version 1.1 3 | 4 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 5 | 660 York Street, Suite 102, 6 | San Francisco, CA 94110 USA 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this 9 | license document, but changing it is not allowed. 10 | 11 | 12 | Developer's Certificate of Origin 1.1 13 | 14 | By making a contribution to this project, I certify that: 15 | 16 | (a) The contribution was created in whole or in part by me and I 17 | have the right to submit it under the open source license 18 | indicated in the file; or 19 | 20 | (b) The contribution is based upon previous work that, to the best 21 | of my knowledge, is covered under an appropriate open source 22 | license and I have the right under that license to submit that 23 | work with modifications, whether created in whole or in part 24 | by me, under the same open source license (unless I am 25 | permitted to submit under a different license), as indicated 26 | in the file; or 27 | 28 | (c) The contribution was provided directly to me by some other 29 | person who certified (a), (b) or (c) and I have not modified 30 | it. 31 | 32 | (d) I understand and agree that this project and the contribution 33 | are public and that a record of the contribution (including all 34 | personal information I submit with it, including my sign-off) is 35 | maintained indefinitely and may be redistributed consistent with 36 | this project or the open source license(s) involved. 37 | -------------------------------------------------------------------------------- /MAINTAINERS: -------------------------------------------------------------------------------- 1 | The maintainers are generally available in Slack at 2 | https://cloud-native.slack.com in #flux (https://cloud-native.slack.com/messages/CLAJ40HV3) 3 | (obtain an invitation at https://slack.cncf.io/). 4 | 5 | In alphabetical order: 6 | 7 | Kingdon Barrett, Weaveworks (github: @kingdonb, slack: Kingdon B) 8 | Hidde Beydals, Weaveworks (github: @hiddeco, slack: hidde) 9 | Michael Bridgen, Weaveworks (github: @squaremo, slack: Michael Bridgen) 10 | Stefan Prodan, Weaveworks (github: @stefanprodan, slack: stefanprodan) 11 | 12 | Retired maintainers: 13 | 14 | - Alfonso Acosta 15 | - Nick Cabatoff 16 | - Justin Barrick 17 | 18 | Thank you for your involvement, and let us not say "farewell" ... 19 | -------------------------------------------------------------------------------- /bin/kubeyaml: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | docker run --rm -i squaremo/kubeyaml:0.7.0 "$@" 3 | -------------------------------------------------------------------------------- /bin/upload-binaries: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ## Uploads the built binaries to the GH release 3 | ## Requires github-release 4 | 5 | set -e 6 | 7 | GITHUB_USER=${GITHUB_USER:-"${CIRCLE_PROJECT_USERNAME}"} 8 | GITHUB_REPO=${GITHUB_REPO:-"${CIRCLE_PROJECT_REPONAME}"} 9 | GITHUB_TAG=${GITHUB_TAG:-"${CIRCLE_TAG}"} 10 | 11 | function do_publish() { 12 | os=$1 13 | arch=$2 14 | echo "= Uploading fluxctl_${os}_${arch} to GH release ${GITHUB_TAG}" 15 | github-release upload \ 16 | --user ${GITHUB_USER} \ 17 | --repo ${GITHUB_REPO} \ 18 | --tag ${GITHUB_TAG} \ 19 | --name "fluxctl_${os}_${arch}" \ 20 | --file "build/fluxctl_${os}_${arch}" 21 | echo "* Finished pushing fluxctl_${os}_${arch} for ${GITHUB_TAG}" 22 | } 23 | 24 | #amd64 25 | for os in linux darwin windows; do 26 | do_publish $os amd64 27 | done 28 | 29 | #arm 30 | for arch in arm arm64; do 31 | do_publish linux $arch 32 | done 33 | 34 | -------------------------------------------------------------------------------- /chart/README.md: -------------------------------------------------------------------------------- 1 | # Flux chart 2 | 3 | This directory contains a chart for flux. See [the README.md there](./flux/README.md) for 4 | instructions on how to use it. 5 | 6 | ## Publishing new charts 7 | 8 | We use GitHub pages to publish the chart as a versioned package. The 9 | tarballs and index.yaml file are updated with a script in that branch. 10 | -------------------------------------------------------------------------------- /chart/flux/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | -------------------------------------------------------------------------------- /chart/flux/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: "1.25.4" 3 | version: 1.13.3 4 | kubeVersion: ">=1.16.0-0" 5 | name: flux 6 | description: Flux is a tool that automatically ensures that the state of a cluster matches what is specified in version control 7 | home: https://fluxcd.io 8 | sources: 9 | - https://github.com/fluxcd/flux 10 | maintainers: 11 | - name: stefanprodan 12 | email: stefan@weave.works 13 | - name: kingdonb 14 | email: kingdon@weave.works 15 | engine: gotpl 16 | icon: https://raw.githubusercontent.com/fluxcd/flux/master/docs/_files/weave-flux.png 17 | keywords: 18 | - gitops 19 | -------------------------------------------------------------------------------- /chart/flux/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | Get the Git deploy key by either (a) running 2 | 3 | kubectl -n {{ .Release.Namespace }} logs deployment/{{ .Release.Name }} | grep identity.pub | cut -d '"' -f2 4 | 5 | or by (b) installing fluxctl through 6 | https://fluxcd.io/legacy/flux/references/fluxctl/#installing-fluxctl 7 | and running: 8 | 9 | fluxctl identity --k8s-fwd-ns {{ .Release.Namespace }} 10 | 11 | --- 12 | 13 | **Flux v1 is deprecated, please upgrade to v2 as soon as possible!** 14 | 15 | New users of Flux can Get Started here: 16 | https://fluxcd.io/flux/get-started/ 17 | 18 | Existing users can upgrade using the Migration Guide: 19 | https://fluxcd.io/flux/migration/ 20 | -------------------------------------------------------------------------------- /chart/flux/templates/configmap-dashboards.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.dashboards.enabled -}} 2 | {{- $nameprefix := .Values.dashboards.nameprefix }} 3 | {{- $namespace := .Values.dashboards.namespace | default $.Release.Namespace }} 4 | {{- $files := .Files }} 5 | {{- range $path, $_ := .Files.Glob "dashboards/*.json" }} 6 | {{- $filename := trimSuffix (ext $path) (base $path) }} 7 | apiVersion: v1 8 | kind: ConfigMap 9 | metadata: 10 | name: {{ $nameprefix }}-{{ $filename }} 11 | namespace: {{ $namespace }} 12 | labels: 13 | grafana_dashboard: "1" 14 | app: {{ template "flux.name" $ }} 15 | chart: {{ template "flux.chart" $ }} 16 | release: {{ $.Release.Name }} 17 | heritage: {{ $.Release.Service }} 18 | data: 19 | {{ base $path }}: '{{ $files.Get $path }}' 20 | --- 21 | {{- end }} 22 | {{- end -}} 23 | -------------------------------------------------------------------------------- /chart/flux/templates/gitconfig.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.git.config.enabled -}} 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: {{ include "git.config.secretName" . }} 6 | namespace: {{ .Release.Namespace }} 7 | type: Opaque 8 | data: 9 | gitconfig: {{ .Values.git.config.data | b64enc }} 10 | {{- end -}} 11 | -------------------------------------------------------------------------------- /chart/flux/templates/kube.yaml: -------------------------------------------------------------------------------- 1 | {{- if not .Values.kube.externalConfig }} 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: {{ template "flux.fullname" . }}-kube-config 6 | namespace: {{ .Release.Namespace }} 7 | data: 8 | config: | 9 | {{- if not .Values.clusterRole.create }} 10 | apiVersion: v1 11 | clusters: [] 12 | contexts: 13 | - context: 14 | cluster: "" 15 | namespace: {{ .Release.Namespace }} 16 | user: "" 17 | name: default 18 | current-context: default 19 | kind: Config 20 | preferences: {} 21 | users: [] 22 | {{- else if .Values.kube.config }} 23 | {{- if contains "\n" .Values.kube.config }} 24 | {{- range $value := .Values.kube.config | splitList "\n" }} 25 | {{ print $value }} 26 | {{- end }} 27 | {{- else }} 28 | {{ .Values.kube.config }} 29 | {{- end }} 30 | {{- end }} 31 | {{- end }} 32 | -------------------------------------------------------------------------------- /chart/flux/templates/psp.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.pspEnabled }} 2 | apiVersion: policy/v1beta1 3 | kind: PodSecurityPolicy 4 | metadata: 5 | name: {{ template "flux.fullname" . }} 6 | namespace: {{ .Release.Namespace }} 7 | labels: 8 | app: {{ include "flux.name" . }} 9 | chart: {{ include "flux.chart" . }} 10 | release: {{ .Release.Name }} 11 | heritage: {{ .Release.Service }} 12 | annotations: 13 | seccomp.security.alpha.kubernetes.io/allowedProfileNames: '*' 14 | spec: 15 | privileged: false 16 | hostIPC: false 17 | hostNetwork: false 18 | hostPID: false 19 | readOnlyRootFilesystem: false 20 | allowPrivilegeEscalation: true 21 | allowedCapabilities: 22 | - '*' 23 | fsGroup: 24 | rule: RunAsAny 25 | runAsUser: 26 | rule: RunAsAny 27 | seLinux: 28 | rule: RunAsAny 29 | supplementalGroups: 30 | rule: RunAsAny 31 | volumes: 32 | - '*' 33 | --- 34 | kind: ClusterRole 35 | apiVersion: rbac.authorization.k8s.io/v1 36 | metadata: 37 | name: {{ template "flux.fullname" . }}-psp 38 | labels: 39 | app: {{ include "flux.name" . }} 40 | chart: {{ include "flux.chart" . }} 41 | release: {{ .Release.Name }} 42 | heritage: {{ .Release.Service }} 43 | rules: 44 | - apiGroups: ['policy'] 45 | resources: ['podsecuritypolicies'] 46 | verbs: ['use'] 47 | resourceNames: 48 | - {{ template "flux.fullname" . }} 49 | --- 50 | apiVersion: rbac.authorization.k8s.io/v1 51 | kind: ClusterRoleBinding 52 | metadata: 53 | name: {{ template "flux.fullname" . }}-psp 54 | labels: 55 | app: {{ include "flux.name" . }} 56 | chart: {{ include "flux.chart" . }} 57 | release: {{ .Release.Name }} 58 | heritage: {{ .Release.Service }} 59 | roleRef: 60 | apiGroup: rbac.authorization.k8s.io 61 | kind: ClusterRole 62 | name: {{ template "flux.fullname" . }}-psp 63 | subjects: 64 | - kind: ServiceAccount 65 | name: {{ template "flux.serviceAccountName" . }} 66 | namespace: {{ .Release.Namespace }} 67 | {{- end }} 68 | -------------------------------------------------------------------------------- /chart/flux/templates/rbac-role.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.rbac.create (eq .Values.clusterRole.create false) -}} 2 | {{- range $namespace := (append .Values.allowedNamespaces .Release.Namespace) }} 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: Role 5 | metadata: 6 | name: {{ template "flux.fullname" $ }} 7 | namespace: {{ $namespace }} 8 | labels: 9 | app: {{ template "flux.name" $ }} 10 | chart: {{ template "flux.chart" $ }} 11 | release: {{ $.Release.Name }} 12 | heritage: {{ $.Release.Service }} 13 | rules: 14 | - apiGroups: 15 | - '*' 16 | resources: 17 | - '*' 18 | verbs: 19 | - '*' 20 | --- 21 | apiVersion: rbac.authorization.k8s.io/v1 22 | kind: RoleBinding 23 | metadata: 24 | name: {{ template "flux.fullname" $ }} 25 | namespace: {{ $namespace }} 26 | labels: 27 | app: {{ template "flux.name" $ }} 28 | chart: {{ template "flux.chart" $ }} 29 | release: {{ $.Release.Name }} 30 | heritage: {{ $.Release.Service }} 31 | roleRef: 32 | apiGroup: rbac.authorization.k8s.io 33 | kind: Role 34 | name: {{ template "flux.fullname" $ }} 35 | subjects: 36 | - name: {{ template "flux.serviceAccountName" $ }} 37 | namespace: {{ $.Release.Namespace | quote }} 38 | kind: ServiceAccount 39 | --- 40 | {{- end }} 41 | apiVersion: rbac.authorization.k8s.io/v1 42 | kind: ClusterRole 43 | metadata: 44 | name: {{ template "flux.fullname" . }}-crd 45 | labels: 46 | app: {{ template "flux.name" . }} 47 | chart: {{ template "flux.chart" . }} 48 | release: {{ .Release.Name }} 49 | heritage: {{ .Release.Service }} 50 | rules: 51 | - apiGroups: 52 | - apiextensions.k8s.io 53 | resources: 54 | - customresourcedefinitions 55 | verbs: 56 | - list 57 | - watch 58 | - apiGroups: 59 | - "" 60 | resources: 61 | - namespaces 62 | verbs: 63 | - list 64 | --- 65 | apiVersion: rbac.authorization.k8s.io/v1 66 | kind: ClusterRoleBinding 67 | metadata: 68 | name: {{ template "flux.fullname" . }} 69 | labels: 70 | app: {{ template "flux.name" . }} 71 | chart: {{ template "flux.chart" . }} 72 | release: {{ .Release.Name }} 73 | heritage: {{ .Release.Service }} 74 | roleRef: 75 | apiGroup: rbac.authorization.k8s.io 76 | kind: ClusterRole 77 | name: {{ template "flux.fullname" . }}-crd 78 | subjects: 79 | - name: {{ template "flux.serviceAccountName" . }} 80 | namespace: {{ .Release.Namespace | quote }} 81 | kind: ServiceAccount 82 | {{- end -}} 83 | -------------------------------------------------------------------------------- /chart/flux/templates/rbac.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create -}} 2 | {{if .Values.clusterRole.create -}} 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: ClusterRole 5 | metadata: 6 | name: {{ template "flux.clusterRoleName" . }} 7 | labels: 8 | app: {{ template "flux.name" . }} 9 | chart: {{ template "flux.chart" . }} 10 | release: {{ .Release.Name }} 11 | heritage: {{ .Release.Service }} 12 | rules: 13 | - apiGroups: 14 | - '*' 15 | resources: 16 | - '*' 17 | verbs: 18 | - '*' 19 | - nonResourceURLs: 20 | - '*' 21 | verbs: 22 | - '*' 23 | {{- end -}} 24 | {{- if or .Values.clusterRole.create .Values.clusterRole.name }} 25 | --- 26 | apiVersion: rbac.authorization.k8s.io/v1 27 | kind: ClusterRoleBinding 28 | metadata: 29 | name: {{ template "flux.clusterRoleName" . }} 30 | labels: 31 | app: {{ template "flux.name" . }} 32 | chart: {{ template "flux.chart" . }} 33 | release: {{ .Release.Name }} 34 | heritage: {{ .Release.Service }} 35 | roleRef: 36 | apiGroup: rbac.authorization.k8s.io 37 | kind: ClusterRole 38 | name: {{ template "flux.clusterRoleName" . }} 39 | subjects: 40 | - name: {{ template "flux.serviceAccountName" . }} 41 | namespace: {{ .Release.Namespace | quote }} 42 | kind: ServiceAccount 43 | {{- end -}} 44 | {{- end -}} 45 | -------------------------------------------------------------------------------- /chart/flux/templates/secret.yaml: -------------------------------------------------------------------------------- 1 | {{- if not .Values.git.secretName -}} 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: {{ template "flux.fullname" . }}-git-deploy 6 | namespace: {{ .Release.Namespace }} 7 | {{- if .Values.ssh.secret.annotations }} 8 | annotations: {{ toYaml .Values.ssh.secret.annotations | nindent 4 }} 9 | {{- end }} 10 | type: Opaque 11 | {{- end -}} 12 | -------------------------------------------------------------------------------- /chart/flux/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ template "flux.fullname" . }} 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | app: {{ template "flux.name" . }} 8 | chart: {{ template "flux.chart" . }} 9 | release: {{ .Release.Name }} 10 | heritage: {{ .Release.Service }} 11 | spec: 12 | type: {{ .Values.service.type }} 13 | {{- if and (eq .Values.service.type "ClusterIP") (eq .Values.service.createClusterIP false) }} 14 | clusterIP: None 15 | {{- end }} 16 | ports: 17 | - port: {{ .Values.service.port }} 18 | targetPort: http 19 | protocol: TCP 20 | name: http 21 | selector: 22 | app: {{ template "flux.name" . }} 23 | release: {{ .Release.Name }} 24 | -------------------------------------------------------------------------------- /chart/flux/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ template "flux.serviceAccountName" . }} 6 | namespace: {{ .Release.Namespace }} 7 | labels: 8 | app: {{ template "flux.name" . }} 9 | chart: {{ template "flux.chart" . }} 10 | release: {{ .Release.Name }} 11 | heritage: {{ .Release.Service }} 12 | {{- if .Values.serviceAccount.annotations }} 13 | annotations: {{ toYaml .Values.serviceAccount.annotations | nindent 4 }} 14 | {{- end }} 15 | {{- end -}} 16 | -------------------------------------------------------------------------------- /chart/flux/templates/servicemonitor.yaml: -------------------------------------------------------------------------------- 1 | {{ if .Values.prometheus.serviceMonitor.create }} 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: ServiceMonitor 4 | metadata: 5 | name: {{ template "flux.fullname" . }} 6 | labels: 7 | app: {{ template "flux.name" . }} 8 | chart: {{ template "flux.chart" . }} 9 | release: {{ .Release.Name }} 10 | heritage: {{ .Release.Service }} 11 | {{- range $key, $value := .Values.prometheus.serviceMonitor.additionalLabels }} 12 | {{ $key }}: {{ $value | quote }} 13 | {{- end }} 14 | {{- with .Values.prometheus.serviceMonitor.namespace }} 15 | namespace: {{ . }} 16 | {{- end }} 17 | spec: 18 | endpoints: 19 | - port: http 20 | honorLabels: true 21 | {{- with .Values.prometheus.serviceMonitor.interval }} 22 | interval: {{ . }} 23 | {{- end }} 24 | {{- with .Values.prometheus.serviceMonitor.scrapeTimeout }} 25 | scrapeTimeout: {{ . }} 26 | {{- end }} 27 | namespaceSelector: 28 | matchNames: 29 | - {{ .Release.Namespace }} 30 | selector: 31 | matchLabels: 32 | app: {{ template "flux.name" . }} 33 | release: {{ .Release.Name }} 34 | {{- end }} 35 | -------------------------------------------------------------------------------- /chart/flux/templates/ssh.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ssh.known_hosts -}} 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: {{ template "flux.fullname" . }}-ssh-config 6 | namespace: {{ .Release.Namespace }} 7 | data: 8 | known_hosts: | 9 | {{- if .Values.ssh.known_hosts }} 10 | {{- if contains "\n" .Values.ssh.known_hosts }} 11 | {{- range $value := .Values.ssh.known_hosts | splitList "\n" }} 12 | {{ print $value }} 13 | {{- end }} 14 | {{- else }} 15 | {{ .Values.ssh.known_hosts }} 16 | {{- end }} 17 | {{- end }} 18 | {{- if .Values.ssh.config }} 19 | config: | 20 | {{- if contains "\n" .Values.ssh.config }} 21 | {{- range $value := .Values.ssh.config | splitList "\n" }} 22 | {{ print $value }} 23 | {{- end }} 24 | {{- else }} 25 | {{ .Values.ssh.config }} 26 | {{- end }} 27 | {{- end }} 28 | {{- end -}} 29 | -------------------------------------------------------------------------------- /cmd/fluxctl/args_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "testing" 8 | ) 9 | 10 | func helperCommand(command string, s ...string) (cmd *exec.Cmd) { 11 | cs := []string{"-test.run=TestHelperProcess", "--", command} 12 | cs = append(cs, s...) 13 | cmd = exec.Command(os.Args[0], cs...) 14 | cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"} 15 | return cmd 16 | } 17 | 18 | func TestHelperProcess(t *testing.T) { 19 | if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { 20 | return 21 | } 22 | defer os.Exit(0) 23 | 24 | args := os.Args 25 | for len(args) > 0 { 26 | if args[0] == "--" { 27 | args = args[1:] 28 | break 29 | } 30 | args = args[1:] 31 | } 32 | if len(args) == 0 { 33 | t.Fatalf("No command\n") 34 | } 35 | 36 | _, args = args[0], args[1:] 37 | for _, a := range args { 38 | if a == "user.name" { 39 | fmt.Fprintf(os.Stdout, "Jane Doe") 40 | } else if a == "user.email" { 41 | fmt.Fprintf(os.Stdout, "jd@j.d") 42 | } 43 | } 44 | } 45 | 46 | func checkAuthor(t *testing.T, input string, expected string) { 47 | execCommand = helperCommand 48 | defer func() { execCommand = exec.Command }() 49 | author := getUserGitConfigValue(input) 50 | if author != expected { 51 | t.Fatalf("author %q does not match expected value %q", author, expected) 52 | } 53 | } 54 | 55 | func TestGetCommitAuthor_OnlyName(t *testing.T) { 56 | checkAuthor(t, "user.name", "Jane Doe") 57 | } 58 | 59 | func TestGetCommitAuthor_OnlyEmail(t *testing.T) { 60 | checkAuthor(t, "user.email", "jd@j.d") 61 | } 62 | -------------------------------------------------------------------------------- /cmd/fluxctl/automate_cmd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/fluxcd/flux/pkg/update" 7 | ) 8 | 9 | type workloadAutomateOpts struct { 10 | *rootOpts 11 | namespace string 12 | workload string 13 | outputOpts 14 | cause update.Cause 15 | 16 | // Deprecated 17 | controller string 18 | } 19 | 20 | func newWorkloadAutomate(parent *rootOpts) *workloadAutomateOpts { 21 | return &workloadAutomateOpts{rootOpts: parent} 22 | } 23 | 24 | func (opts *workloadAutomateOpts) Command() *cobra.Command { 25 | cmd := &cobra.Command{ 26 | Use: "automate", 27 | Short: "Turn on automatic deployment for a workload.", 28 | Example: makeExample( 29 | "fluxctl automate --workload=default:deployment/helloworld", 30 | ), 31 | RunE: opts.RunE, 32 | } 33 | AddOutputFlags(cmd, &opts.outputOpts) 34 | AddCauseFlags(cmd, &opts.cause) 35 | cmd.Flags().StringVarP(&opts.namespace, "namespace", "n", "", "Workload namespace") 36 | cmd.Flags().StringVarP(&opts.workload, "workload", "w", "", "Workload to automate") 37 | 38 | // Deprecated 39 | cmd.Flags().StringVarP(&opts.controller, "controller", "c", "", "Controller to automate") 40 | cmd.Flags().MarkDeprecated("controller", "changed to --workload, use that instead") 41 | 42 | return cmd 43 | } 44 | 45 | func (opts *workloadAutomateOpts) RunE(cmd *cobra.Command, args []string) error { 46 | // Backwards compatibility with --controller until we remove it 47 | switch { 48 | case opts.workload != "" && opts.controller != "": 49 | return newUsageError("can't specify both the controller and workload") 50 | case opts.controller != "": 51 | opts.workload = opts.controller 52 | } 53 | ns := getKubeConfigContextNamespaceOrDefault(opts.namespace, "default", opts.Context) 54 | policyOpts := &workloadPolicyOpts{ 55 | rootOpts: opts.rootOpts, 56 | outputOpts: opts.outputOpts, 57 | namespace: ns, 58 | workload: opts.workload, 59 | cause: opts.cause, 60 | automate: true, 61 | } 62 | return policyOpts.RunE(cmd, args) 63 | } 64 | -------------------------------------------------------------------------------- /cmd/fluxctl/completion_cmd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "sort" 7 | "strings" 8 | 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var ( 13 | completionShells = map[string]func(out io.Writer, cmd *cobra.Command) error{ 14 | "bash": runCompletionBash, 15 | "zsh": runCompletionZsh, 16 | "fish": runCompletionFish, 17 | } 18 | ) 19 | 20 | func newCompletionCommand() *cobra.Command { 21 | shells := []string{} 22 | for s := range completionShells { 23 | shells = append(shells, s) 24 | } 25 | 26 | return &cobra.Command{ 27 | Use: "completion SHELL", 28 | DisableFlagsInUseLine: true, 29 | Short: "Output shell completion code for the specified shell (bash, zsh, or fish)", 30 | RunE: func(cmd *cobra.Command, args []string) error { 31 | if len(args) == 0 { 32 | return newUsageError("please specify a shell") 33 | } 34 | 35 | if len(args) > 1 { 36 | sort.Strings(shells) 37 | return newUsageError(fmt.Sprintf("please specify one of the following shells: %s", strings.Join(shells, " "))) 38 | } 39 | 40 | run, found := completionShells[args[0]] 41 | if !found { 42 | return newUsageError(fmt.Sprintf("unsupported shell type %q", args[0])) 43 | } 44 | 45 | return run(cmd.OutOrStdout(), cmd) 46 | }, 47 | ValidArgs: shells, 48 | } 49 | } 50 | 51 | func runCompletionBash(out io.Writer, fluxctl *cobra.Command) error { 52 | return fluxctl.Root().GenBashCompletion(out) 53 | } 54 | 55 | func runCompletionZsh(out io.Writer, fluxctl *cobra.Command) error { 56 | return fluxctl.Root().GenZshCompletion(out) 57 | } 58 | 59 | func runCompletionFish(out io.Writer, fluxctl *cobra.Command) error { 60 | return fluxctl.Root().GenFishCompletion(out, true) 61 | } 62 | -------------------------------------------------------------------------------- /cmd/fluxctl/completion_cmd_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestCompletionCommand_InputFailure(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | args []string 15 | expected error 16 | }{ 17 | { 18 | name : "no argument", 19 | args : []string{}, 20 | expected: fmt.Errorf("please specify a shell"), 21 | }, 22 | { 23 | name : "invalid shell option", 24 | args : []string{"foo"}, 25 | expected: fmt.Errorf("unsupported shell type \"foo\""), 26 | }, 27 | { 28 | name : "multiple shell options", 29 | args : []string{"bash", "zsh", "fish"}, 30 | expected: fmt.Errorf("please specify one of the following shells: bash fish zsh"), 31 | }, 32 | } 33 | 34 | for _, tt := range tests { 35 | t.Run(tt.name, func(t *testing.T) { 36 | cmd := newCompletionCommand() 37 | cmd.SetArgs(tt.args) 38 | err := cmd.Execute() 39 | assert.Error(t, err) 40 | assert.Equal(t, tt.expected.Error(), err.Error()) 41 | }) 42 | } 43 | } 44 | 45 | func TestCompletionCommand_Success(t *testing.T) { 46 | tests := []struct { 47 | shell string 48 | expected string 49 | }{ 50 | { 51 | shell: "bash", 52 | expected: "bash completion for completion", 53 | }, 54 | { 55 | shell: "zsh", 56 | expected: "compdef _completion completion", 57 | }, 58 | { 59 | shell: "fish", 60 | expected: "fish completion for completion", 61 | }, 62 | } 63 | 64 | for _, tt := range tests { 65 | t.Run(tt.shell, func(t *testing.T) { 66 | cmd := newCompletionCommand() 67 | buf := new(bytes.Buffer) 68 | cmd.SetArgs([]string{tt.shell}) 69 | cmd.SetOut(buf) 70 | err := cmd.Execute() 71 | assert.NoError(t, err) 72 | assert.Contains(t, buf.String(), tt.expected) 73 | }) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /cmd/fluxctl/deautomate_cmd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/fluxcd/flux/pkg/update" 7 | ) 8 | 9 | type workloadDeautomateOpts struct { 10 | *rootOpts 11 | namespace string 12 | workload string 13 | outputOpts 14 | cause update.Cause 15 | 16 | // Deprecated 17 | controller string 18 | } 19 | 20 | func newWorkloadDeautomate(parent *rootOpts) *workloadDeautomateOpts { 21 | return &workloadDeautomateOpts{rootOpts: parent} 22 | } 23 | 24 | func (opts *workloadDeautomateOpts) Command() *cobra.Command { 25 | cmd := &cobra.Command{ 26 | Use: "deautomate", 27 | Short: "Turn off automatic deployment for a workload.", 28 | Example: makeExample( 29 | "fluxctl deautomate --workload=default:deployment/helloworld", 30 | ), 31 | RunE: opts.RunE, 32 | } 33 | AddOutputFlags(cmd, &opts.outputOpts) 34 | AddCauseFlags(cmd, &opts.cause) 35 | cmd.Flags().StringVarP(&opts.namespace, "namespace", "n", "", "Workload namespace") 36 | cmd.Flags().StringVarP(&opts.workload, "workload", "w", "", "Workload to deautomate") 37 | 38 | // Deprecated 39 | cmd.Flags().StringVarP(&opts.controller, "controller", "c", "", "Controller to deautomate") 40 | cmd.Flags().MarkDeprecated("controller", "changed to --workload, use that instead") 41 | 42 | return cmd 43 | } 44 | 45 | func (opts *workloadDeautomateOpts) RunE(cmd *cobra.Command, args []string) error { 46 | // Backwards compatibility with --controller until we remove it 47 | switch { 48 | case opts.workload != "" && opts.controller != "": 49 | return newUsageError("can't specify both a controller and workload") 50 | case opts.controller != "": 51 | opts.workload = opts.controller 52 | } 53 | ns := getKubeConfigContextNamespaceOrDefault(opts.namespace, "default", opts.Context) 54 | policyOpts := &workloadPolicyOpts{ 55 | rootOpts: opts.rootOpts, 56 | outputOpts: opts.outputOpts, 57 | namespace: ns, 58 | workload: opts.workload, 59 | cause: opts.cause, 60 | deautomate: true, 61 | } 62 | return policyOpts.RunE(cmd, args) 63 | } 64 | -------------------------------------------------------------------------------- /cmd/fluxctl/error.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | type usageError struct { 8 | error 9 | } 10 | 11 | func newUsageError(msg string) usageError { 12 | return usageError{error: errors.New(msg)} 13 | } 14 | 15 | func checkExactlyOne(optsDescription string, supplied ...bool) error { 16 | found := false 17 | for _, s := range supplied { 18 | if found && s { 19 | return newUsageError("please supply only one of " + optsDescription) 20 | } 21 | found = found || s 22 | } 23 | 24 | if !found { 25 | return newUsageError("please supply exactly one of " + optsDescription) 26 | } 27 | 28 | return nil 29 | } 30 | 31 | var errorWantedNoArgs = newUsageError("expected no (non-flag) arguments") 32 | var errorInvalidOutputFormat = newUsageError("invalid output format specified") 33 | -------------------------------------------------------------------------------- /cmd/fluxctl/identity_cmd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | type identityOpts struct { 11 | *rootOpts 12 | regenerate bool 13 | fingerprint bool 14 | visual bool 15 | } 16 | 17 | func newIdentity(parent *rootOpts) *identityOpts { 18 | return &identityOpts{rootOpts: parent} 19 | } 20 | 21 | func (opts *identityOpts) Command() *cobra.Command { 22 | cmd := &cobra.Command{ 23 | Use: "identity", 24 | Short: "Display SSH public key", 25 | RunE: opts.RunE, 26 | } 27 | cmd.Flags().BoolVarP(&opts.regenerate, "regenerate", "r", false, `Generate a new identity`) 28 | cmd.Flags().BoolVarP(&opts.fingerprint, "fingerprint", "l", false, `Show fingerprint of public key`) 29 | cmd.Flags().BoolVarP(&opts.visual, "visual", "v", false, `Show ASCII art representation with fingerprint (implies -l)`) 30 | return cmd 31 | } 32 | 33 | func (opts *identityOpts) RunE(_ *cobra.Command, args []string) error { 34 | if len(args) > 0 { 35 | return errorWantedNoArgs 36 | } 37 | 38 | ctx := context.Background() 39 | 40 | repoConfig, err := opts.API.GitRepoConfig(ctx, opts.regenerate) 41 | if err != nil { 42 | return err 43 | } 44 | publicSSHKey := repoConfig.PublicSSHKey 45 | 46 | if opts.visual { 47 | opts.fingerprint = true 48 | } 49 | 50 | if opts.fingerprint { 51 | fmt.Println(publicSSHKey.Fingerprints["md5"].Hash) 52 | if opts.visual { 53 | fmt.Print(publicSSHKey.Fingerprints["md5"].Randomart) 54 | } 55 | } else { 56 | fmt.Print(publicSSHKey.Key) 57 | } 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /cmd/fluxctl/install_cmd_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | func TestInstallCommand_ExtraArgumentFailure(t *testing.T) { 10 | for k, v := range [][]string{ 11 | {"foo"}, 12 | {"foo", "bar"}, 13 | {"foo", "bar", "bizz"}, 14 | {"foo", "bar", "bizz", "buzz"}, 15 | } { 16 | t.Run(fmt.Sprintf("%d", k), func(t *testing.T) { 17 | cmd := newInstall().Command() 18 | buf := new(bytes.Buffer) 19 | cmd.SetOut(buf) 20 | cmd.SetArgs(v) 21 | _ = cmd.Flags().Set("git-url", "git@github.com:testcase/flux-get-started") 22 | _ = cmd.Flags().Set("git-email", "testcase@weave.works") 23 | if err := cmd.Execute(); err == nil { 24 | t.Fatalf("expecting error, got nil") 25 | } 26 | }) 27 | } 28 | } 29 | 30 | func TestInstallCommand_MissingRequiredFlag(t *testing.T) { 31 | for k, v := range map[string]string{ 32 | "git-url": "git@github.com:testcase/flux-get-started", 33 | "git-email": "testcase@weave.works", 34 | } { 35 | t.Run(fmt.Sprintf("only --%s", k), func(t *testing.T) { 36 | cmd := newInstall().Command() 37 | buf := new(bytes.Buffer) 38 | cmd.SetOut(buf) 39 | cmd.SetArgs([]string{}) 40 | _ = cmd.Flags().Set(k, v) 41 | if err := cmd.Execute(); err == nil { 42 | t.Fatalf("expecting error, got nil") 43 | } 44 | }) 45 | } 46 | } 47 | 48 | func TestInstallCommand_Success(t *testing.T) { 49 | f := make(map[string]string) 50 | f["git-url"] = "git@github.com:testcase/flux-get-started" 51 | f["git-email"] = "testcase@weave.works" 52 | 53 | cmd := newInstall().Command() 54 | buf := new(bytes.Buffer) 55 | cmd.SetOut(buf) 56 | cmd.SetArgs([]string{}) 57 | for k, v := range f { 58 | _ = cmd.Flags().Set(k, v) 59 | } 60 | if err := cmd.Execute(); err != nil { 61 | t.Fatalf("expecting nil, got error (%s)", err.Error()) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /cmd/fluxctl/list_workloads_cmd_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | 11 | "github.com/fluxcd/flux/pkg/cluster" 12 | 13 | "github.com/fluxcd/flux/pkg/resource" 14 | 15 | v6 "github.com/fluxcd/flux/pkg/api/v6" 16 | ) 17 | 18 | func Test_outputWorkloadsJson(t *testing.T) { 19 | buf := &bytes.Buffer{} 20 | 21 | t.Run("sends JSON to the io.Writer", func(t *testing.T) { 22 | workloads := testWorkloads(5) 23 | err := outputWorkloadsJson(workloads, buf) 24 | require.NoError(t, err) 25 | unmarshallTarget := &[]v6.ControllerStatus{} 26 | err = json.Unmarshal(buf.Bytes(), unmarshallTarget) 27 | require.NoError(t, err) 28 | }) 29 | } 30 | 31 | func testWorkloads(workloadCount int) []v6.ControllerStatus { 32 | workloads := []v6.ControllerStatus{} 33 | for i := 0; i < workloadCount; i++ { 34 | name := fmt.Sprintf("mah-app-%d", i) 35 | id := resource.MakeID("applications", "deployment", name) 36 | 37 | cs := v6.ControllerStatus{ 38 | ID: id, 39 | Containers: nil, 40 | ReadOnly: "", 41 | Status: "ready", 42 | Rollout: cluster.RolloutStatus{}, 43 | SyncError: "", 44 | Antecedent: resource.ID{}, 45 | Labels: nil, 46 | Automated: false, 47 | Locked: false, 48 | Ignore: false, 49 | Policies: nil, 50 | } 51 | 52 | workloads = append(workloads, cs) 53 | 54 | } 55 | return workloads 56 | } 57 | -------------------------------------------------------------------------------- /cmd/fluxctl/lock_cmd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/fluxcd/flux/pkg/update" 7 | ) 8 | 9 | type workloadLockOpts struct { 10 | *rootOpts 11 | namespace string 12 | workload string 13 | outputOpts 14 | cause update.Cause 15 | 16 | // Deprecated 17 | controller string 18 | } 19 | 20 | func newWorkloadLock(parent *rootOpts) *workloadLockOpts { 21 | return &workloadLockOpts{rootOpts: parent} 22 | } 23 | 24 | func (opts *workloadLockOpts) Command() *cobra.Command { 25 | cmd := &cobra.Command{ 26 | Use: "lock", 27 | Short: "Lock a workload, so it cannot be deployed.", 28 | Example: makeExample( 29 | "fluxctl lock --workload=default:deployment/helloworld", 30 | ), 31 | RunE: opts.RunE, 32 | } 33 | AddOutputFlags(cmd, &opts.outputOpts) 34 | AddCauseFlags(cmd, &opts.cause) 35 | cmd.Flags().StringVarP(&opts.namespace, "namespace", "n", "", "Controller namespace") 36 | cmd.Flags().StringVarP(&opts.workload, "workload", "w", "", "Workload to lock") 37 | 38 | // Deprecated 39 | cmd.Flags().StringVarP(&opts.workload, "controller", "c", "", "Controller to lock") 40 | cmd.Flags().MarkDeprecated("controller", "changed to --workload, use that instead") 41 | 42 | return cmd 43 | } 44 | 45 | func (opts *workloadLockOpts) RunE(cmd *cobra.Command, args []string) error { 46 | // Backwards compatibility with --controller until we remove it 47 | switch { 48 | case opts.workload != "" && opts.controller != "": 49 | return newUsageError("can't specify both a controller and workload") 50 | case opts.controller != "": 51 | opts.workload = opts.controller 52 | } 53 | ns := getKubeConfigContextNamespaceOrDefault(opts.namespace, "default", opts.Context) 54 | policyOpts := &workloadPolicyOpts{ 55 | rootOpts: opts.rootOpts, 56 | outputOpts: opts.outputOpts, 57 | namespace: ns, 58 | workload: opts.workload, 59 | cause: opts.cause, 60 | lock: true, 61 | } 62 | return policyOpts.RunE(cmd, args) 63 | } 64 | -------------------------------------------------------------------------------- /cmd/fluxctl/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/pkg/errors" 7 | 8 | fluxerr "github.com/fluxcd/flux/pkg/errors" 9 | ) 10 | 11 | func run(args []string) int { 12 | rootCmd := newRoot().Command() 13 | rootCmd.SetArgs(args) 14 | if cmd, err := rootCmd.ExecuteC(); err != nil { 15 | // Format flux-specific errors. They can come wrapped, 16 | // so we use the cause instead. 17 | if cause, ok := errors.Cause(err).(*fluxerr.Error); ok { 18 | cmd.Println("== Error ==\n\n" + cause.Help) 19 | } else { 20 | cmd.Println("Error: " + err.Error()) 21 | cmd.Printf("Run '%v --help' for usage.\n", cmd.CommandPath()) 22 | } 23 | return 1 24 | } 25 | return 0 26 | } 27 | 28 | func main() { 29 | os.Exit(run(os.Args[1:])) 30 | } 31 | -------------------------------------------------------------------------------- /cmd/fluxctl/unlock_cmd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/fluxcd/flux/pkg/update" 7 | ) 8 | 9 | type workloadUnlockOpts struct { 10 | *rootOpts 11 | namespace string 12 | workload string 13 | outputOpts 14 | cause update.Cause 15 | 16 | // Deprecated 17 | controller string 18 | } 19 | 20 | func newWorkloadUnlock(parent *rootOpts) *workloadUnlockOpts { 21 | return &workloadUnlockOpts{rootOpts: parent} 22 | } 23 | 24 | func (opts *workloadUnlockOpts) Command() *cobra.Command { 25 | cmd := &cobra.Command{ 26 | Use: "unlock", 27 | Short: "Unlock a workload, so it can be deployed.", 28 | Example: makeExample( 29 | "fluxctl unlock --workload=default:deployment/helloworld", 30 | ), 31 | RunE: opts.RunE, 32 | } 33 | AddOutputFlags(cmd, &opts.outputOpts) 34 | AddCauseFlags(cmd, &opts.cause) 35 | cmd.Flags().StringVarP(&opts.namespace, "namespace", "n", "", "Controller namespace") 36 | cmd.Flags().StringVarP(&opts.workload, "workload", "w", "", "Controller to unlock") 37 | 38 | // Deprecated 39 | cmd.Flags().StringVarP(&opts.controller, "controller", "c", "", "Controller to unlock") 40 | cmd.Flags().MarkDeprecated("controller", "changed to --workload, use that instead") 41 | 42 | return cmd 43 | } 44 | 45 | func (opts *workloadUnlockOpts) RunE(cmd *cobra.Command, args []string) error { 46 | // Backwards compatibility with --controller until we remove it 47 | switch { 48 | case opts.workload != "" && opts.controller != "": 49 | return newUsageError("can't specify both a controller and workload") 50 | case opts.controller != "": 51 | opts.workload = opts.controller 52 | } 53 | ns := getKubeConfigContextNamespaceOrDefault(opts.namespace, "default", opts.Context) 54 | policyOpts := &workloadPolicyOpts{ 55 | rootOpts: opts.rootOpts, 56 | outputOpts: opts.outputOpts, 57 | namespace: ns, 58 | workload: opts.workload, 59 | cause: opts.cause, 60 | unlock: true, 61 | } 62 | return policyOpts.RunE(cmd, args) 63 | } 64 | -------------------------------------------------------------------------------- /cmd/fluxctl/version_cmd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var version string 10 | 11 | func newVersionCommand() *cobra.Command { 12 | return &cobra.Command{ 13 | Use: "version", 14 | Short: "Output the version of fluxctl", 15 | RunE: func(cmd *cobra.Command, args []string) error { 16 | if len(args) != 0 { 17 | return errorWantedNoArgs 18 | } 19 | if version == "" { 20 | version = "unversioned" 21 | } 22 | fmt.Fprintln(cmd.OutOrStdout(), version) 23 | return nil 24 | }, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /cmd/fluxctl/version_cmd_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestVersionCommand_InputFailure(t *testing.T) { 11 | for k, v := range [][]string{ 12 | {"foo"}, 13 | {"foo", "bar"}, 14 | {"foo", "bar", "bizz"}, 15 | {"foo", "bar", "bizz", "buzz"}, 16 | } { 17 | t.Run(fmt.Sprintf("%d", k), func(t *testing.T) { 18 | buf := new(bytes.Buffer) 19 | cmd := newVersionCommand() 20 | cmd.SetOut(buf) 21 | cmd.SetArgs(v) 22 | if err := cmd.Execute(); err == nil { 23 | t.Fatalf("Expecting error: command is not expecting extra arguments") 24 | } 25 | }) 26 | } 27 | } 28 | 29 | func TestVersionCommand_Success(t *testing.T) { 30 | buf := new(bytes.Buffer) 31 | cmd := newVersionCommand() 32 | cmd.SetOut(buf) 33 | cmd.SetArgs([]string{}) 34 | if err := cmd.Execute(); err != nil { 35 | t.Fatalf("Expecting nil, got error (%s)", err.Error()) 36 | } 37 | } 38 | 39 | func TestVersionCommand_SuccessCheckVersion(t *testing.T) { 40 | for _, e := range []string{ 41 | "v1.0.0", 42 | "v2.0.0", 43 | } { 44 | t.Run(e, func(t *testing.T) { 45 | buf := new(bytes.Buffer) 46 | cmd := newVersionCommand() 47 | version = e 48 | cmd.SetOut(buf) 49 | cmd.SetArgs([]string{}) 50 | if err := cmd.Execute(); err != nil { 51 | t.Fatalf("Expecting nil, got error (%s)", err.Error()) 52 | } 53 | if g := strings.TrimRight(buf.String(), "\n"); e != g { 54 | println(e == g) 55 | t.Fatalf("Expected %s, got %s", e, g) 56 | } 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /deploy/flux-account.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # The service account, cluster roles, and cluster role binding are 3 | # only needed for Kubernetes with role-based access control (RBAC). 4 | apiVersion: v1 5 | kind: ServiceAccount 6 | metadata: 7 | labels: 8 | name: flux 9 | name: flux 10 | namespace: flux 11 | --- 12 | apiVersion: rbac.authorization.k8s.io/v1 13 | kind: ClusterRole 14 | metadata: 15 | labels: 16 | name: flux 17 | name: flux 18 | rules: 19 | - apiGroups: ['*'] 20 | resources: ['*'] 21 | verbs: ['*'] 22 | - nonResourceURLs: ['*'] 23 | verbs: ['*'] 24 | --- 25 | apiVersion: rbac.authorization.k8s.io/v1 26 | kind: ClusterRoleBinding 27 | metadata: 28 | labels: 29 | name: flux 30 | name: flux 31 | roleRef: 32 | apiGroup: rbac.authorization.k8s.io 33 | kind: ClusterRole 34 | name: flux 35 | subjects: 36 | - kind: ServiceAccount 37 | name: flux 38 | namespace: flux 39 | -------------------------------------------------------------------------------- /deploy/flux-ns.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | name: flux 6 | -------------------------------------------------------------------------------- /deploy/flux-secret.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: flux-git-deploy 6 | namespace: flux 7 | type: Opaque 8 | -------------------------------------------------------------------------------- /deploy/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - flux-ns.yaml 3 | - memcache-svc.yaml 4 | - memcache-dep.yaml 5 | - flux-account.yaml 6 | - flux-secret.yaml 7 | - flux-deployment.yaml 8 | -------------------------------------------------------------------------------- /deploy/memcache-dep.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # memcached deployment used by Flux to cache 3 | # container image metadata. 4 | apiVersion: apps/v1 5 | kind: Deployment 6 | metadata: 7 | name: memcached 8 | namespace: flux 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | name: memcached 14 | template: 15 | metadata: 16 | labels: 17 | name: memcached 18 | spec: 19 | nodeSelector: 20 | beta.kubernetes.io/os: linux 21 | containers: 22 | - name: memcached 23 | image: memcached:1.6.10-alpine 24 | imagePullPolicy: IfNotPresent 25 | args: 26 | - -m 512 # Maximum memory to use, in megabytes 27 | - -I 5m # Maximum size for one item 28 | - -p 11211 # Default port 29 | # - -vv # Uncomment to get logs of each request and response. 30 | ports: 31 | - name: clients 32 | containerPort: 11211 33 | securityContext: 34 | runAsUser: 11211 35 | runAsGroup: 11211 36 | allowPrivilegeEscalation: false 37 | -------------------------------------------------------------------------------- /deploy/memcache-svc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: memcached 6 | namespace: flux 7 | spec: 8 | ports: 9 | - name: memcached 10 | port: 11211 11 | selector: 12 | name: memcached 13 | -------------------------------------------------------------------------------- /docker/Dockerfile.fluxctl: -------------------------------------------------------------------------------- 1 | FROM alpine:3.15 2 | 3 | WORKDIR /home/flux 4 | 5 | RUN apk add --no-cache openssh-client ca-certificates curl 6 | 7 | COPY ./fluxctl /usr/local/bin/ 8 | 9 | LABEL maintainer="Flux CD " \ 10 | org.opencontainers.image.title="fluxctl" \ 11 | org.opencontainers.image.description="Flux CLI" \ 12 | org.opencontainers.image.url="https://github.com/fluxcd/flux" \ 13 | org.opencontainers.image.source="git@github.com:fluxcd/flux" \ 14 | org.opencontainers.image.vendor="Flux CD" \ 15 | org.label-schema.schema-version="1.0" \ 16 | org.label-schema.name="fluxctl" \ 17 | org.label-schema.description="Flux CLI" \ 18 | org.label-schema.url="https://github.com/fluxcd/flux" \ 19 | org.label-schema.vcs-url="git@github.com:fluxcd/flux" \ 20 | org.label-schema.vendor="Flux CD" 21 | 22 | ARG BUILD_DATE 23 | ARG VCS_REF 24 | 25 | LABEL org.opencontainers.image.revision="$VCS_REF" \ 26 | org.opencontainers.image.created="$BUILD_DATE" \ 27 | org.label-schema.vcs-ref="$VCS_REF" \ 28 | org.label-schema.build-date="$BUILD_DATE" 29 | -------------------------------------------------------------------------------- /docker/image-tag: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | OUTPUT=--quiet 8 | if [ "${1:-}" = '--show-diff' ]; then 9 | OUTPUT= 10 | fi 11 | 12 | # If a tagged version, just print that tag 13 | HEAD_TAGS=( $(git tag --points-at HEAD) ) 14 | case ${#HEAD_TAGS[@]} in 15 | 0) ;; 16 | 17 | 1) echo ${HEAD_TAGS[0]}; exit 0 18 | ;; 19 | 20 | 2) # For releases we may have two tags, one with an v prefix (e.g. v1.17.0 and 1.17.0), 21 | # discard the v prefix 22 | TAG1=${HEAD_TAGS[0]} 23 | TAG2=${HEAD_TAGS[1]} 24 | if [[ "${TAG1}" == v* && ${TAG1%$TAG2} == "v" ]]; then 25 | echo ${TAG2}; exit 0 26 | fi 27 | if [[ "${TAG2}" == v* && ${TAG2%$TAG1} == "v" ]]; then 28 | echo ${TAG1}; exit 0 29 | fi 30 | echo "error: more than one tag pointing to HEAD" >&2; exit 1; ;; 31 | 32 | *) echo "error: more than one tag pointing to HEAD" >&2; exit 1; ;; 33 | esac 34 | 35 | 36 | 37 | WORKING_SUFFIX=$(if ! git diff --exit-code ${OUTPUT} HEAD >&2; \ 38 | then echo "-wip"; \ 39 | else echo ""; \ 40 | fi) 41 | BRANCH_PREFIX=$(git rev-parse --abbrev-ref HEAD) 42 | 43 | # replace spaces with dash 44 | BRANCH_PREFIX=${BRANCH_PREFIX// /-} 45 | # next, replace slashes with dash 46 | BRANCH_PREFIX=${BRANCH_PREFIX//[\/\\]/-} 47 | # now, clean out anything that's not alphanumeric or an dash 48 | BRANCH_PREFIX=${BRANCH_PREFIX//[^a-zA-Z0-9-]/} 49 | # finally, lowercase with TR 50 | BRANCH_PREFIX=`echo -n $BRANCH_PREFIX | tr A-Z a-z` 51 | 52 | echo "$BRANCH_PREFIX-$(git rev-parse --short HEAD)$WORKING_SUFFIX" 53 | -------------------------------------------------------------------------------- /docker/kubeconfig: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | clusters: [] 3 | contexts: 4 | - context: 5 | cluster: "" 6 | namespace: default 7 | user: "" 8 | name: default 9 | current-context: default 10 | kind: Config 11 | preferences: {} 12 | users: [] 13 | -------------------------------------------------------------------------------- /docker/kubectl.version: -------------------------------------------------------------------------------- 1 | KUBECTL_VERSION=v1.21.14 2 | KUBECTL_CHECKSUM_amd64=52a98cc64abeea4187391cbf0ad5bdd69b6920c2b29b8f9afad194441e642fb8f252e14a91c095ef1e85a23e5bb587916bd319566b6e8d1e03be5505400f44b4 3 | KUBECTL_CHECKSUM_arm=fb204c3494bb7acf59ea0dc2b6f94c5a7d2b7e6d69c05b7a7e77b3fc438e574e9ac5f8720a4ae8f2f660822262aa8c4723bfe269c49ed641031f8259210e5b48 4 | KUBECTL_CHECKSUM_arm64=ed613592035b45c4f4571eed2b739c837def4287aacc7c37665e596484aa6dbd299224ef12ec329cfa8a655e20b6bee6f6fc3204bc79b92d832171073ddde191 5 | -------------------------------------------------------------------------------- /docker/kustomize.version: -------------------------------------------------------------------------------- 1 | # Any version above 3.8.7 results in regression explained in fluxcd/flux#3500. 2 | KUSTOMIZE_VERSION=3.8.7 3 | KUSTOMIZE_CHECKSUM=4a3372d7bfdffe2eaf729e77f88bc94ce37dc84de55616bfe90aac089bf6fd02 4 | -------------------------------------------------------------------------------- /docker/sops.version: -------------------------------------------------------------------------------- 1 | SOPS_VERSION=v3.7.3 2 | SOPS_CHECKSUM=53aec65e45f62a769ff24b7e5384f0c82d62668dd96ed56685f649da114b4dbb 3 | -------------------------------------------------------------------------------- /docker/ssh_config: -------------------------------------------------------------------------------- 1 | Host * 2 | StrictHostKeyChecking yes 3 | IdentityFile /etc/fluxd/ssh/identity 4 | IdentityFile /var/fluxd/keygen/identity 5 | LogLevel error 6 | HostkeyAlgorithms +ssh-rsa 7 | PubkeyAcceptedAlgorithms +ssh-rsa 8 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Flux 2 | 3 | > **🛑 Upgrade Advisory** 4 | > 5 | > This documentation is for Flux (v1) which has [reached its end-of-life in November 2022](https://fluxcd.io/blog/2022/10/september-2022-update/#flux-legacy-v1-retirement-plan). 6 | > 7 | > We strongly recommend you familiarise yourself with the newest Flux and [migrate as soon as possible](https://fluxcd.io/flux/migration/). 8 | > 9 | > For documentation regarding the latest Flux, please refer to [this section](https://fluxcd.io/flux/). 10 | 11 | ![Flux v1 Diagram](_files/flux-cd-diagram.png) 12 | 13 | Flux is a tool that automatically ensures that the state of a cluster matches 14 | the config in git. It uses [an operator](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/) 15 | in the cluster to trigger deployments inside Kubernetes, which means you don't 16 | need a separate CD tool. It monitors all relevant image repositories, detects 17 | new images, triggers deployments and updates the desired running configuration 18 | based on that (and a configurable policy). 19 | 20 | The benefits are: you don't need to grant your CI access to the cluster, every 21 | change is atomic and transactional, git has your audit log. Each transaction 22 | either fails or succeeds cleanly. You're entirely code centric and don't need 23 | new infrastructure. 24 | 25 | ## Get started 26 | 27 | With the following tutorials: 28 | 29 | - [Get started with Flux](tutorials/get-started.md) 30 | - [Get started with Flux using Helm](tutorials/get-started-helm.md) 31 | 32 | Making use of Helm charts in your cluster? Combine Flux with the [Helm 33 | Operator](https://github.com/fluxcd/helm-operator) to declaratively manage chart 34 | releases using `HelmRelease` custom resources. 35 | 36 | For progressive delivery patterns like Canary Releases, A/B Testing and Blue/Green, 37 | Flux can be used together with [Flagger](https://fluxcd.io/flagger). 38 | 39 | ## Getting help 40 | 41 | If you have any questions about Flux and continuous delivery: 42 | 43 | - Invite yourself to the CNCF community 44 | slack and ask a question on the [#flux](https://cloud-native.slack.com/messages/flux/) 45 | channel. 46 | - To be part of the conversation about Flux's development, join the 47 | [flux-dev mailing list](https://lists.cncf.io/g/cncf-flux-dev). 48 | - [File an issue.](https://github.com/fluxcd/flux/issues/new/choose) 49 | 50 | Your feedback is always welcome! 51 | 52 | -------------------------------------------------------------------------------- /docs/_files/flux-cd-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluxcd/flux/c2317f813d94474579aab0827a7f746c7674b199/docs/_files/flux-cd-diagram.png -------------------------------------------------------------------------------- /docs/_files/weave-flux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluxcd/flux/c2317f813d94474579aab0827a7f746c7674b199/docs/_files/weave-flux.png -------------------------------------------------------------------------------- /docs/contributing/building.md: -------------------------------------------------------------------------------- 1 | # Building Flux 2 | 3 | > **🛑 Upgrade Advisory** 4 | > 5 | > This documentation is for Flux (v1) which has [reached its end-of-life in November 2022](https://fluxcd.io/blog/2022/10/september-2022-update/#flux-legacy-v1-retirement-plan). 6 | > 7 | > We strongly recommend you familiarise yourself with the newest Flux and [migrate as soon as possible](https://fluxcd.io/flux/migration/). 8 | > 9 | > For documentation regarding the latest Flux, please refer to [this section](https://fluxcd.io/flux/). 10 | 11 | You'll need a working `go` environment version >= 1.11 (official releases are built against `1.13`). 12 | It's also expected that you have a Docker daemon for building images. 13 | 14 | Clone the repository. The project uses [Go Modules](https://github.com/golang/go/wiki/Modules), 15 | so if you explicitly define `$GOPATH` you should clone somewhere else. 16 | 17 | Then, from the root directory: 18 | 19 | ```sh 20 | make 21 | ``` 22 | 23 | This makes Docker images, and installs binaries to `$GOBIN` (if you define it) or `$(go env GOPATH)/bin`. 24 | 25 | > ⚠ Note: 26 | > The default target architecture is amd64. If you would like 27 | > to try to build Docker images and binaries for a different 28 | > architecture you will have to set ARCH variable: 29 | > 30 | > ```sh 31 | > make ARCH= 32 | > ``` 33 | 34 | ## Running tests 35 | 36 | ```sh 37 | # Unit tests 38 | make test 39 | 40 | # End-to-end tests 41 | make e2e 42 | ``` 43 | -------------------------------------------------------------------------------- /docs/get-started.md: -------------------------------------------------------------------------------- 1 | # Get started 2 | 3 | > **🛑 Upgrade Advisory** 4 | > 5 | > This documentation is for Flux (v1) which has [reached its end-of-life in November 2022](https://fluxcd.io/blog/2022/10/september-2022-update/#flux-legacy-v1-retirement-plan). 6 | > 7 | > We strongly recommend you familiarise yourself with the newest Flux and [migrate as soon as possible](https://fluxcd.io/flux/migration/). 8 | > 9 | > For documentation regarding the latest Flux, please refer to [this section](https://fluxcd.io/flux/). 10 | 11 | All you need is a Kubernetes cluster and a git repo. The git repo 12 | contains [manifests](https://kubernetes.io/docs/concepts/configuration/overview/) 13 | (as YAML files) describing what should run in the cluster. Flux imposes 14 | [some requirements](requirements.md) on these files. 15 | 16 | ## Installing Flux 17 | 18 | Here are the instructions to [install Flux on your own 19 | cluster](tutorials/get-started.md). 20 | 21 | If you are using Helm, we have a [separate section about 22 | this](tutorials/get-started-helm.md). 23 | -------------------------------------------------------------------------------- /docs/guides/provide-own-ssh-key.md: -------------------------------------------------------------------------------- 1 | # Providing your own SSH key 2 | 3 | > **🛑 Upgrade Advisory** 4 | > 5 | > This documentation is for Flux (v1) which has [reached its end-of-life in November 2022](https://fluxcd.io/blog/2022/10/september-2022-update/#flux-legacy-v1-retirement-plan). 6 | > 7 | > We strongly recommend you familiarise yourself with the newest Flux and [migrate as soon as possible](https://fluxcd.io/flux/migration/). 8 | > 9 | > For documentation regarding the latest Flux, please refer to [this section](https://fluxcd.io/flux/). 10 | 11 | Flux connects to the repository using an SSH key it retrieves from a 12 | Kubernetes secret, if the configured (`--k8s-secret-name`) secret has 13 | no `identity` key/value pair, it will generate new private key. 14 | 15 | With this knowledge, providing your own SSH key is as simple as 16 | creating the configured secret in the expected format. 17 | 18 | 1. create a Kubernetes secret from your own private key: 19 | 20 | ```sh 21 | kubectl create secret generic flux-git-deploy --from-file=identity=/full/path/to/private_key 22 | ``` 23 | 24 | this will result in a secret that has the structure: 25 | 26 | ```yaml 27 | apiVersion: v1 28 | data: 29 | identity: 30 | kind: Secret 31 | type: Opaque 32 | metadata: 33 | ... 34 | ``` 35 | 36 | 2. _(optional)_ if you created the secret with a non-default name 37 | (default: `flux-git-deploy`), set the `--k8s-secret-name` flag to 38 | the name of your secret (i.e. `--k8s-secret-name=foo`). 39 | 40 | > ⚠ Note: 41 | > The SSH key must be configured to have R/W access to the 42 | > repository. More specifically, the SSH key must be able to create 43 | > and update tags. E.g. in Gitlab, that means it requires `Maintainer` 44 | > permissions. The `Developer` permission can create tags, but not 45 | > update them. 46 | -------------------------------------------------------------------------------- /docs/requirements.md: -------------------------------------------------------------------------------- 1 | # Requirements and limitations 2 | 3 | > **🛑 Upgrade Advisory** 4 | > 5 | > This documentation is for Flux (v1) which has [reached its end-of-life in November 2022](https://fluxcd.io/blog/2022/10/september-2022-update/#flux-legacy-v1-retirement-plan). 6 | > 7 | > We strongly recommend you familiarise yourself with the newest Flux and [migrate as soon as possible](https://fluxcd.io/flux/migration/). 8 | > 9 | > For documentation regarding the latest Flux, please refer to [this section](https://fluxcd.io/flux/). 10 | 11 | Flux has some requirements of the files it finds in your git repo. 12 | 13 | * Flux can only deal with one such repo at a time. This limitation is 14 | technical and may go away. 15 | 16 | * Flux only deals with YAML files at present. It tries to preserve 17 | comments and whitespace in YAMLs when updating them. You may see 18 | updates with incidental, harmless changes, like reindented blocks. 19 | 20 | * Flux will ignore directories that look like Helm charts, to avoid 21 | applying templated YAML manifests. A directory will be skipped if 22 | its contents include the files `Chart.yaml` and `values.yaml`, as 23 | these are the (only) mandatory components of a Helm chart. 24 | 25 | It is _not_ a requirement that the files are arranged in any 26 | particular way into directories. Flux will look in subdirectories for 27 | YAML files recursively, but does not infer any meaning from the 28 | directory structure. 29 | 30 | Flux uses the Docker Registry API to collect metadata about the images 31 | running in the cluster. This comes with at least one limitation: 32 | 33 | * Since Flux runs in a container in your cluster, it may not be able 34 | to resolve all hostnames that you or Kubernetes can resolve. In 35 | particular, it won't be able to get image metadata for images in a 36 | private image registry that's made available at `localhost`. 37 | -------------------------------------------------------------------------------- /pkg/api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "github.com/fluxcd/flux/pkg/api/v11" 4 | 5 | // Server defines the minimal interface a Flux must satisfy to adequately serve a 6 | // connecting fluxctl. This interface specifically does not facilitate connecting 7 | // to Weave Cloud. 8 | type Server interface { 9 | v11.Server 10 | } 11 | -------------------------------------------------------------------------------- /pkg/api/v10/api.go: -------------------------------------------------------------------------------- 1 | // This package defines the types for Flux API version 10. 2 | package v10 3 | 4 | import ( 5 | "context" 6 | 7 | "github.com/fluxcd/flux/pkg/api/v6" 8 | "github.com/fluxcd/flux/pkg/api/v9" 9 | "github.com/fluxcd/flux/pkg/update" 10 | ) 11 | 12 | type ListImagesOptions struct { 13 | Spec update.ResourceSpec 14 | OverrideContainerFields []string 15 | Namespace string 16 | } 17 | 18 | type Server interface { 19 | v6.NotDeprecated 20 | 21 | ListImagesWithOptions(ctx context.Context, opts ListImagesOptions) ([]v6.ImageStatus, error) 22 | } 23 | 24 | type Upstream interface { 25 | v9.Upstream 26 | } 27 | -------------------------------------------------------------------------------- /pkg/api/v11/api.go: -------------------------------------------------------------------------------- 1 | // This package defines the types for Flux API version 11. 2 | package v11 3 | 4 | import ( 5 | "context" 6 | 7 | "github.com/fluxcd/flux/pkg/api/v10" 8 | "github.com/fluxcd/flux/pkg/api/v6" 9 | "github.com/fluxcd/flux/pkg/resource" 10 | ) 11 | 12 | type ListServicesOptions struct { 13 | Namespace string 14 | Services []resource.ID 15 | } 16 | 17 | type Server interface { 18 | v10.Server 19 | 20 | ListServicesWithOptions(ctx context.Context, opts ListServicesOptions) ([]v6.ControllerStatus, error) 21 | 22 | // NB Upstream methods move into the public API, since 23 | // weaveworks/flux-adapter now relies on the public API 24 | v10.Upstream 25 | } 26 | -------------------------------------------------------------------------------- /pkg/api/v9/api.go: -------------------------------------------------------------------------------- 1 | // This package defines the types for Flux API version 9. 2 | package v9 3 | 4 | import ( 5 | "context" 6 | 7 | "github.com/fluxcd/flux/pkg/api/v6" 8 | ) 9 | 10 | type Server interface { 11 | v6.NotDeprecated 12 | } 13 | 14 | type Upstream interface { 15 | v6.Upstream 16 | 17 | // ChangeNotify tells the daemon that we've noticed a change in 18 | // e.g., the git repo, or image registry, and now would be a good 19 | // time to update its state. 20 | NotifyChange(context.Context, Change) error 21 | } 22 | -------------------------------------------------------------------------------- /pkg/api/v9/change.go: -------------------------------------------------------------------------------- 1 | package v9 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | 7 | "github.com/fluxcd/flux/pkg/image" 8 | ) 9 | 10 | type ChangeKind string 11 | 12 | const ( 13 | GitChange ChangeKind = "git" 14 | ImageChange ChangeKind = "image" 15 | ) 16 | 17 | func (k ChangeKind) MarshalJSON() ([]byte, error) { 18 | return json.Marshal(string(k)) 19 | } 20 | 21 | var ErrUnknownChange = errors.New("unknown kind of change") 22 | 23 | type Change struct { 24 | Kind ChangeKind // essentially a type tag 25 | Source interface{} // what changed 26 | } 27 | 28 | func (c *Change) UnmarshalJSON(bs []byte) error { 29 | type raw struct { 30 | Kind ChangeKind 31 | Source json.RawMessage 32 | } 33 | var r raw 34 | var err error 35 | if err = json.Unmarshal(bs, &r); err != nil { 36 | return err 37 | } 38 | c.Kind = r.Kind 39 | 40 | switch r.Kind { 41 | case GitChange: 42 | var git GitUpdate 43 | err = json.Unmarshal(r.Source, &git) 44 | c.Source = git 45 | case ImageChange: 46 | var image ImageUpdate 47 | err = json.Unmarshal(r.Source, &image) 48 | c.Source = image 49 | default: 50 | return ErrUnknownChange 51 | } 52 | return err 53 | } 54 | 55 | type ImageUpdate struct { 56 | Name image.Name 57 | } 58 | 59 | type GitUpdate struct { 60 | URL, Branch string 61 | } 62 | -------------------------------------------------------------------------------- /pkg/api/v9/change_test.go: -------------------------------------------------------------------------------- 1 | package v9 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/fluxcd/flux/pkg/image" 9 | ) 10 | 11 | func TestChangeEncoding(t *testing.T) { 12 | ref, _ := image.ParseRef("docker.io/fluxcd/flux") 13 | name := ref.Name 14 | 15 | for _, update := range []Change{ 16 | {Kind: GitChange, Source: GitUpdate{URL: "git@github.com:fluxcd/flux"}}, 17 | {Kind: ImageChange, Source: ImageUpdate{Name: name}}, 18 | } { 19 | bytes, err := json.Marshal(update) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | var update2 Change 24 | if err = json.Unmarshal(bytes, &update2); err != nil { 25 | t.Fatal(err) 26 | } 27 | if !reflect.DeepEqual(update, update2) { 28 | t.Errorf("unmarshaled != original.\nExpected: %#v\nGot: %#v", update, update2) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pkg/checkpoint/checkpoint.go: -------------------------------------------------------------------------------- 1 | package checkpoint 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/go-kit/kit/log" 7 | ) 8 | 9 | const ( 10 | versionCheckPeriod = 6 * time.Hour 11 | ) 12 | 13 | func CheckForUpdates(product, version string, extra map[string]string, logger log.Logger) *checker { 14 | handleResponse := func() { 15 | logger.Log("msg", "Flux v1 is deprecated, please upgrade to v2", "latest", "v2", "URL", "https://fluxcd.io/flux/migration/") 16 | } 17 | 18 | flags := map[string]string{ 19 | "kernel-version": "XXXXX", 20 | } 21 | for k, v := range extra { 22 | flags[k] = v 23 | } 24 | 25 | params := checkParams{ 26 | Product: product, 27 | Version: version, 28 | SignatureFile: "", 29 | Flags: flags, 30 | } 31 | 32 | return checkInterval(¶ms, versionCheckPeriod, handleResponse) 33 | } 34 | 35 | func checkInterval(p *checkParams, interval time.Duration, 36 | cb func()) *checker { 37 | 38 | state := &checker{ 39 | doneCh: make(chan struct{}), 40 | } 41 | 42 | if isCheckDisabled() { 43 | return state 44 | } 45 | 46 | go func() { 47 | cb() 48 | 49 | for { 50 | after := randomStagger(interval) 51 | state.nextCheckAtLock.Lock() 52 | state.nextCheckAt = time.Now().Add(after) 53 | state.nextCheckAtLock.Unlock() 54 | 55 | select { 56 | case <-time.After(after): 57 | cb() 58 | case <-state.doneCh: 59 | return 60 | } 61 | } 62 | }() 63 | 64 | return state 65 | } 66 | -------------------------------------------------------------------------------- /pkg/checkpoint/types.go: -------------------------------------------------------------------------------- 1 | package checkpoint 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | type flag struct { 9 | Key string 10 | Value string 11 | } 12 | 13 | type checkParams struct { 14 | Product string 15 | Version string 16 | Flags map[string]string 17 | ExtraFlags func() []flag 18 | Arch string 19 | OS string 20 | Signature string 21 | SignatureFile string 22 | CacheFile string 23 | CacheDuration time.Duration 24 | Force bool 25 | } 26 | 27 | type checker struct { 28 | doneCh chan struct{} 29 | nextCheckAt time.Time 30 | nextCheckAtLock sync.RWMutex 31 | } 32 | -------------------------------------------------------------------------------- /pkg/checkpoint/util.go: -------------------------------------------------------------------------------- 1 | package checkpoint 2 | 3 | import ( 4 | mrand "math/rand" 5 | "os" 6 | "time" 7 | ) 8 | 9 | func isCheckDisabled() bool { 10 | return os.Getenv("CHECKPOINT_DISABLE") != "" 11 | } 12 | 13 | func randomStagger(interval time.Duration) time.Duration { 14 | stagger := time.Duration(mrand.Int63()) % (interval / 2) 15 | return 3*(interval/4) + stagger 16 | } 17 | -------------------------------------------------------------------------------- /pkg/cluster/includelist.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | import ( 4 | "github.com/ryanuber/go-glob" 5 | ) 6 | 7 | // This is to represent "include-exclude" predicate, which is used 8 | // for deciding images to scan. 9 | 10 | type Includer interface { 11 | IsIncluded(string) bool 12 | } 13 | 14 | type IncluderFunc func(string) bool 15 | 16 | func (f IncluderFunc) IsIncluded(s string) bool { 17 | return f(s) 18 | } 19 | 20 | var AlwaysInclude = IncluderFunc(func(string) bool { return true }) 21 | 22 | // ExcludeIncludeGlob is an Includer that uses glob patterns to decide 23 | // what to include or exclude. Note that Include and Exclude are 24 | // treated differently -- see the method IsIncluded. 25 | type ExcludeIncludeGlob struct { 26 | Include []string 27 | Exclude []string 28 | } 29 | 30 | // IsIncluded implements Includer using the logic: 31 | // - if the string matches any exclude pattern, don't include it 32 | // - otherwise, if there are no include patterns, include it 33 | // - otherwise, if it matches an include pattern, include it 34 | // = otherwise don't include it. 35 | func (ei ExcludeIncludeGlob) IsIncluded(s string) bool { 36 | for _, ex := range ei.Exclude { 37 | if glob.Glob(ex, s) { 38 | return false 39 | } 40 | } 41 | if len(ei.Include) == 0 { 42 | return true 43 | } 44 | for _, in := range ei.Include { 45 | if glob.Glob(in, s) { 46 | return true 47 | } 48 | } 49 | return false 50 | } 51 | -------------------------------------------------------------------------------- /pkg/cluster/includelist_test.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestIncluderFunc(t *testing.T) { 10 | in := IncluderFunc(func(s string) bool { 11 | return s == "included" 12 | }) 13 | assert.True(t, in.IsIncluded("included")) 14 | assert.False(t, in.IsIncluded("excluded")) 15 | } 16 | 17 | func TestExcludeInclude(t *testing.T) { 18 | test := func(ei Includer, s string, expected bool) { 19 | if expected { 20 | t.Run("includes "+s, func(t *testing.T) { 21 | assert.True(t, ei.IsIncluded(s)) 22 | }) 23 | } else { 24 | t.Run("excludes "+s, func(t *testing.T) { 25 | assert.False(t, ei.IsIncluded(s)) 26 | }) 27 | } 28 | } 29 | 30 | // Only exclude stuff 31 | ei1 := ExcludeIncludeGlob{ 32 | Exclude: []string{"foo/*"}, 33 | } 34 | 35 | for _, t := range []string{ 36 | "", 37 | "completely unrelated", 38 | "foo", 39 | "starts/foo/bar", 40 | } { 41 | test(ei1, t, true) 42 | } 43 | 44 | for _, t := range []string{ 45 | "foo/bar", 46 | } { 47 | test(ei1, t, false) 48 | } 49 | 50 | // Explicitly include stuff 51 | ei2 := ExcludeIncludeGlob{ 52 | Exclude: []string{"foo/bar/*"}, 53 | Include: []string{"foo/*", "boo/*"}, 54 | } 55 | 56 | for _, t := range []string{ 57 | "boo/whatever", 58 | "foo/something/else", 59 | } { 60 | test(ei2, t, true) 61 | } 62 | 63 | for _, t := range []string{ 64 | "baz/anything", 65 | "foo/bar/something", 66 | "anything not explicitly included", 67 | } { 68 | test(ei2, t, false) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /pkg/cluster/kubernetes/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package kubernetes provides implementations of `Cluster` and 3 | `manifests` that interact with the Kubernetes API (using kubectl or 4 | the k8s API client). 5 | */ 6 | package kubernetes 7 | -------------------------------------------------------------------------------- /pkg/cluster/kubernetes/errors.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "fmt" 5 | 6 | fluxerr "github.com/fluxcd/flux/pkg/errors" 7 | ) 8 | 9 | func ObjectMissingError(obj string, err error) *fluxerr.Error { 10 | return &fluxerr.Error{ 11 | Type: fluxerr.Missing, 12 | Err: err, 13 | Help: fmt.Sprintf(`Cluster object %q not found 14 | 15 | The object requested was not found in the cluster. Check spelling and 16 | perhaps verify its presence using kubectl. 17 | `, obj)} 18 | } 19 | 20 | func UpdateNotSupportedError(kind string) *fluxerr.Error { 21 | return &fluxerr.Error{ 22 | Type: fluxerr.User, 23 | Err: fmt.Errorf("updating resource kind %q not supported", kind), 24 | Help: `Flux does not support updating ` + kind + ` resources. 25 | 26 | This may be because those resources do not use images, you are trying 27 | to use a YAML dot notation path annotation for a non HelmRelease 28 | resource, or because it is a new kind of resource in Kubernetes, and 29 | Flux does not support it yet. 30 | 31 | If you can use a Deployment instead, Flux can work with 32 | those. Otherwise, you may have to update the resource manually (e.g., 33 | using kubectl). 34 | `, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pkg/cluster/kubernetes/kubernetes_test.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "sort" 7 | "testing" 8 | 9 | "github.com/go-kit/kit/log" 10 | apiv1 "k8s.io/api/core/v1" 11 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | fakekubernetes "k8s.io/client-go/kubernetes/fake" 13 | ) 14 | 15 | func newNamespace(name string) *apiv1.Namespace { 16 | return &apiv1.Namespace{ 17 | ObjectMeta: meta_v1.ObjectMeta{ 18 | Name: name, 19 | }, 20 | TypeMeta: meta_v1.TypeMeta{ 21 | APIVersion: "v1", 22 | Kind: "Namespace", 23 | }, 24 | } 25 | } 26 | 27 | func testGetAllowedNamespaces(t *testing.T, namespace []string, expected []string) { 28 | clientset := fakekubernetes.NewSimpleClientset(newNamespace("default"), 29 | newNamespace("kube-system")) 30 | client := ExtendedClient{coreClient: clientset} 31 | allowedNamespaces := make(map[string]struct{}) 32 | for _, n := range namespace { 33 | allowedNamespaces[n] = struct{}{} 34 | } 35 | c := NewCluster(client, nil, nil, log.NewNopLogger(), allowedNamespaces, nil, []string{}) 36 | 37 | namespaces, err := c.getAllowedAndExistingNamespaces(context.Background()) 38 | if err != nil { 39 | t.Errorf("The error should be nil, not: %s", err) 40 | } 41 | 42 | sort.Strings(namespaces) // We cannot be sure of the namespace order, since they are saved in a map in cluster struct 43 | sort.Strings(expected) 44 | 45 | if reflect.DeepEqual(namespaces, expected) != true { 46 | t.Errorf("Unexpected namespaces: %v != %v.", namespaces, expected) 47 | } 48 | } 49 | 50 | func TestGetAllowedNamespacesDefault(t *testing.T) { 51 | testGetAllowedNamespaces(t, []string{}, []string{""}) // this will be empty string which means all namespaces 52 | } 53 | 54 | func TestGetAllowedNamespacesNamespacesIsNil(t *testing.T) { 55 | testGetAllowedNamespaces(t, nil, []string{""}) // this will be empty string which means all namespaces 56 | } 57 | 58 | func TestGetAllowedNamespacesNamespacesSet(t *testing.T) { 59 | testGetAllowedNamespaces(t, []string{"default"}, []string{"default"}) 60 | } 61 | 62 | func TestGetAllowedNamespacesNamespacesSetDoesNotExist(t *testing.T) { 63 | testGetAllowedNamespaces(t, []string{"hello"}, []string{}) 64 | } 65 | 66 | func TestGetAllowedNamespacesNamespacesMultiple(t *testing.T) { 67 | testGetAllowedNamespaces(t, []string{"default", "hello", "kube-system"}, []string{"default", "kube-system"}) 68 | } 69 | -------------------------------------------------------------------------------- /pkg/cluster/kubernetes/kubeyaml.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "os/exec" 7 | "strings" 8 | ) 9 | 10 | // KubeYAML is a placeholder value for calling the helper executable 11 | // `kubeyaml`. 12 | type KubeYAML struct { 13 | } 14 | 15 | // Image calls the kubeyaml subcommand `image` with the arguments given. 16 | func (k KubeYAML) Image(in []byte, ns, kind, name, container, image string) ([]byte, error) { 17 | args := []string{"image", "--namespace", ns, "--kind", kind, "--name", name} 18 | args = append(args, "--container", container, "--image", image) 19 | return execKubeyaml(in, args) 20 | } 21 | 22 | // Annotate calls the kubeyaml subcommand `annotate` with the arguments as given. 23 | func (k KubeYAML) Annotate(in []byte, ns, kind, name string, policies ...string) ([]byte, error) { 24 | args := []string{"annotate", "--namespace", ns, "--kind", kind, "--name", name} 25 | args = append(args, policies...) 26 | return execKubeyaml(in, args) 27 | } 28 | 29 | // Set calls the kubeyaml subcommand `set` with the arguments given. 30 | func (k KubeYAML) Set(in []byte, ns, kind, name string, values ...string) ([]byte, error) { 31 | args := []string{"set", "--namespace", ns, "--kind", kind, "--name", name} 32 | args = append(args, values...) 33 | return execKubeyaml(in, args) 34 | } 35 | 36 | func execKubeyaml(in []byte, args []string) ([]byte, error) { 37 | cmd := exec.Command("kubeyaml", args...) 38 | out := &bytes.Buffer{} 39 | errOut := &bytes.Buffer{} 40 | cmd.Stdin = bytes.NewBuffer(in) 41 | cmd.Stdout = out 42 | cmd.Stderr = errOut 43 | 44 | err := cmd.Run() 45 | if err != nil { 46 | if errOut.Len() == 0 { 47 | return nil, err 48 | } 49 | return nil, errors.New(strings.TrimSpace(errOut.String())) 50 | } 51 | return out.Bytes(), nil 52 | } 53 | -------------------------------------------------------------------------------- /pkg/cluster/kubernetes/mock.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import kresource "github.com/fluxcd/flux/pkg/cluster/kubernetes/resource" 4 | 5 | type ConstNamespacer string 6 | 7 | func (ns ConstNamespacer) EffectiveNamespace(manifest kresource.KubeManifest, _ ResourceScopes) (string, error) { 8 | return string(ns), nil 9 | } 10 | -------------------------------------------------------------------------------- /pkg/cluster/kubernetes/policies.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "fmt" 5 | 6 | kresource "github.com/fluxcd/flux/pkg/cluster/kubernetes/resource" 7 | "github.com/fluxcd/flux/pkg/resource" 8 | ) 9 | 10 | func (m *manifests) UpdateWorkloadPolicies(def []byte, id resource.ID, update resource.PolicyUpdate) ([]byte, error) { 11 | resources, err := m.ParseManifest(def, "stdin") 12 | if err != nil { 13 | return nil, err 14 | } 15 | res, ok := resources[id.String()] 16 | if !ok { 17 | return nil, fmt.Errorf("resource %s not found", id.String()) 18 | } 19 | 20 | // This is the Kubernetes manifests implementation; panic if it's 21 | // not returning `KubeManifest`s. 22 | kres := res.(kresource.KubeManifest) 23 | 24 | workload, ok := res.(resource.Workload) 25 | if !ok { 26 | return nil, fmt.Errorf("resource %s does not have containers", id.String()) 27 | } 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | changes, err := resource.ChangesForPolicyUpdate(workload, update) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | var args []string 38 | for k, v := range changes { 39 | annotation, ok := kres.PolicyAnnotationKey(k) 40 | if !ok { 41 | annotation = fmt.Sprintf("%s%s", kresource.PolicyPrefix, k) 42 | } 43 | args = append(args, fmt.Sprintf("%s=%s", annotation, v)) 44 | } 45 | 46 | ns, kind, name := id.Components() 47 | return (KubeYAML{}).Annotate(def, ns, kind, name, args...) 48 | } 49 | -------------------------------------------------------------------------------- /pkg/cluster/kubernetes/resource/cronjob.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/fluxcd/flux/pkg/image" 5 | "github.com/fluxcd/flux/pkg/resource" 6 | ) 7 | 8 | type CronJob struct { 9 | baseObject 10 | Spec CronJobSpec 11 | } 12 | 13 | type CronJobSpec struct { 14 | JobTemplate struct { 15 | Spec struct { 16 | Template PodTemplate 17 | } 18 | } `yaml:"jobTemplate"` 19 | } 20 | 21 | func (c CronJob) Containers() []resource.Container { 22 | return c.Spec.JobTemplate.Spec.Template.Containers() 23 | } 24 | 25 | func (c CronJob) SetContainerImage(container string, ref image.Ref) error { 26 | return c.Spec.JobTemplate.Spec.Template.SetContainerImage(container, ref) 27 | } 28 | 29 | var _ resource.Workload = CronJob{} 30 | -------------------------------------------------------------------------------- /pkg/cluster/kubernetes/resource/daemonset.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/fluxcd/flux/pkg/image" 5 | "github.com/fluxcd/flux/pkg/resource" 6 | ) 7 | 8 | type DaemonSet struct { 9 | baseObject 10 | Spec struct { 11 | Template PodTemplate 12 | } 13 | } 14 | 15 | func (ds DaemonSet) Containers() []resource.Container { 16 | return ds.Spec.Template.Containers() 17 | } 18 | 19 | func (ds DaemonSet) SetContainerImage(container string, ref image.Ref) error { 20 | return ds.Spec.Template.SetContainerImage(container, ref) 21 | } 22 | 23 | var _ resource.Workload = DaemonSet{} 24 | -------------------------------------------------------------------------------- /pkg/cluster/kubernetes/resource/deployment.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/fluxcd/flux/pkg/image" 5 | "github.com/fluxcd/flux/pkg/resource" 6 | ) 7 | 8 | type Deployment struct { 9 | baseObject 10 | Spec DeploymentSpec 11 | } 12 | 13 | type DeploymentSpec struct { 14 | Replicas int 15 | Template PodTemplate 16 | } 17 | 18 | func (d Deployment) Containers() []resource.Container { 19 | return d.Spec.Template.Containers() 20 | } 21 | 22 | func (d Deployment) SetContainerImage(container string, ref image.Ref) error { 23 | return d.Spec.Template.SetContainerImage(container, ref) 24 | } 25 | 26 | var _ resource.Workload = Deployment{} 27 | -------------------------------------------------------------------------------- /pkg/cluster/kubernetes/resource/doc.go: -------------------------------------------------------------------------------- 1 | // Types and procedures for representing Kubernetes objects. 2 | // 3 | // These types are mainly for the purpose of turning files and 4 | // exported bytes into resource definitions to send to 5 | // `cluster.Sync`, and ignore much of the detail in "real" Kubernetes 6 | // objects. 7 | 8 | package resource 9 | -------------------------------------------------------------------------------- /pkg/cluster/kubernetes/resource/list.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | type List struct { 4 | baseObject 5 | Items []KubeManifest 6 | } 7 | -------------------------------------------------------------------------------- /pkg/cluster/kubernetes/resource/namespace.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | type Namespace struct { 4 | baseObject 5 | } 6 | -------------------------------------------------------------------------------- /pkg/cluster/kubernetes/resource/spec.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/fluxcd/flux/pkg/image" 7 | "github.com/fluxcd/flux/pkg/resource" 8 | ) 9 | 10 | // Types that daemonsets, deployments, and other things have in 11 | // common. 12 | 13 | type ObjectMeta struct { 14 | Labels map[string]string 15 | Annotations map[string]string 16 | } 17 | 18 | type PodTemplate struct { 19 | Metadata ObjectMeta 20 | Spec PodSpec 21 | } 22 | 23 | func (t PodTemplate) Containers() []resource.Container { 24 | var result []resource.Container 25 | // FIXME(https://github.com/fluxcd/flux/issues/1269): account for possible errors (x2) 26 | for _, c := range t.Spec.Containers { 27 | im, _ := image.ParseRef(c.Image) 28 | result = append(result, resource.Container{Name: c.Name, Image: im}) 29 | } 30 | for _, c := range t.Spec.InitContainers { 31 | im, _ := image.ParseRef(c.Image) 32 | result = append(result, resource.Container{Name: c.Name, Image: im}) 33 | } 34 | return result 35 | } 36 | 37 | func (t PodTemplate) SetContainerImage(container string, ref image.Ref) error { 38 | for i, c := range t.Spec.Containers { 39 | if c.Name == container { 40 | t.Spec.Containers[i].Image = ref.String() 41 | return nil 42 | } 43 | } 44 | for i, c := range t.Spec.InitContainers { 45 | if c.Name == container { 46 | t.Spec.InitContainers[i].Image = ref.String() 47 | return nil 48 | } 49 | } 50 | return fmt.Errorf("container %q not found in workload", container) 51 | } 52 | 53 | type PodSpec struct { 54 | ImagePullSecrets []struct{ Name string } 55 | Volumes []Volume 56 | Containers []ContainerSpec 57 | InitContainers []ContainerSpec `yaml:"initContainers"` 58 | } 59 | 60 | type Volume struct { 61 | Name string 62 | Secret struct { 63 | SecretName string 64 | } 65 | } 66 | 67 | type ContainerSpec struct { 68 | Name string 69 | Image string 70 | Args Args 71 | Ports []ContainerPort 72 | Env Env 73 | } 74 | 75 | type Args []string 76 | 77 | type ContainerPort struct { 78 | ContainerPort int 79 | Name string 80 | } 81 | 82 | type VolumeMount struct { 83 | Name string 84 | MountPath string 85 | ReadOnly bool 86 | } 87 | 88 | // Env is a bag of Name, Value pairs that are treated somewhat like a 89 | // map. 90 | type Env []EnvEntry 91 | 92 | type EnvEntry struct { 93 | Name, Value string 94 | } 95 | -------------------------------------------------------------------------------- /pkg/cluster/kubernetes/resource/statefulset.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/fluxcd/flux/pkg/image" 5 | "github.com/fluxcd/flux/pkg/resource" 6 | ) 7 | 8 | type StatefulSet struct { 9 | baseObject 10 | Spec StatefulSetSpec 11 | } 12 | 13 | type StatefulSetSpec struct { 14 | Replicas int 15 | Template PodTemplate 16 | } 17 | 18 | func (ss StatefulSet) Containers() []resource.Container { 19 | return ss.Spec.Template.Containers() 20 | } 21 | 22 | func (ss StatefulSet) SetContainerImage(container string, ref image.Ref) error { 23 | return ss.Spec.Template.SetContainerImage(container, ref) 24 | } 25 | 26 | var _ resource.Workload = StatefulSet{} 27 | -------------------------------------------------------------------------------- /pkg/cluster/kubernetes/testfiles/data_test.go: -------------------------------------------------------------------------------- 1 | package testfiles 2 | 3 | import ( 4 | "io/ioutil" 5 | "path/filepath" 6 | "testing" 7 | ) 8 | 9 | func TestWriteTestFiles(t *testing.T) { 10 | dir, cleanup := TempDir(t) 11 | defer cleanup() 12 | 13 | if err := WriteTestFiles(dir, Files); err != nil { 14 | cleanup() 15 | t.Fatal(err) 16 | } 17 | 18 | for file, contents := range Files { 19 | var bytes []byte 20 | var err error 21 | if bytes, err = ioutil.ReadFile(filepath.Join(dir, file)); err != nil { 22 | t.Error(err) 23 | } 24 | if string(bytes) != contents { 25 | t.Errorf("file %s has unexpected contents: %q", filepath.Join(dir, file), string(bytes)) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pkg/cluster/kubernetes/update.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | kresource "github.com/fluxcd/flux/pkg/cluster/kubernetes/resource" 8 | "github.com/fluxcd/flux/pkg/image" 9 | "github.com/fluxcd/flux/pkg/resource" 10 | ) 11 | 12 | // updateWorkloadContainer takes a YAML document stream (one or more 13 | // YAML docs, as bytes), a resource ID referring to a controller, a 14 | // container name, and the name of the new image that should be used 15 | // for the container. It returns a new YAML stream where the image for 16 | // the container has been replaced with the imageRef supplied. 17 | func updateWorkloadContainer(in []byte, resource resource.ID, container string, newImageID image.Ref) ([]byte, error) { 18 | namespace, kind, name := resource.Components() 19 | if _, ok := resourceKinds[strings.ToLower(kind)]; !ok { 20 | return nil, UpdateNotSupportedError(kind) 21 | } 22 | return (KubeYAML{}).Image(in, namespace, kind, name, container, newImageID.String()) 23 | } 24 | 25 | // updateWorkloadImagePaths takes a YAML document stream (one or more 26 | // YAML docs, as bytes), a resource ID referring to a HelmRelease, 27 | // a ContainerImageMap, and the name of the new image that should be 28 | // applied to the mapped paths. It returns a new YAML stream where 29 | // the values of the paths have been replaced with the imageRef 30 | // supplied. 31 | func updateWorkloadImagePaths(in []byte, 32 | resource resource.ID, paths kresource.ContainerImageMap, newImageID image.Ref) ([]byte, error) { 33 | namespace, kind, name := resource.Components() 34 | // We only support HelmRelease resource kinds for now 35 | if kind != "helmrelease" { 36 | return nil, UpdateNotSupportedError(kind) 37 | } 38 | if m, ok := paths.MapImageRef(newImageID); ok { 39 | var args []string 40 | for k, v := range m { 41 | args = append(args, fmt.Sprintf("%s=%s", k, v)) 42 | } 43 | return (KubeYAML{}).Set(in, namespace, kind, name, args...) 44 | } 45 | return nil, fmt.Errorf("failed to map paths %#v to %q for %q", paths, newImageID.String(), resource.String()) 46 | } 47 | -------------------------------------------------------------------------------- /pkg/cluster/sync.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/fluxcd/flux/pkg/resource" 7 | ) 8 | 9 | // Definitions for use in synchronising a cluster with a git repo. 10 | 11 | // SyncSet groups the set of resources to be updated. Usually this is 12 | // the set of resources found in a git repo; in any case, it must 13 | // represent the complete set of resources, as garbage collection will 14 | // assume missing resources should be deleted. The name is used to 15 | // distinguish the resources from a set from other resources -- e.g., 16 | // cluster resources not marked as belonging to a set will not be 17 | // deleted by garbage collection. 18 | type SyncSet struct { 19 | Name string 20 | Resources []resource.Resource 21 | } 22 | 23 | type ResourceError struct { 24 | ResourceID resource.ID 25 | Source string 26 | Error error 27 | } 28 | 29 | type SyncError []ResourceError 30 | 31 | func (err SyncError) Error() string { 32 | var errs []string 33 | for _, e := range err { 34 | errs = append(errs, e.ResourceID.String()+": "+e.Error.Error()) 35 | } 36 | return strings.Join(errs, "; ") 37 | } 38 | -------------------------------------------------------------------------------- /pkg/daemon/errors.go: -------------------------------------------------------------------------------- 1 | package daemon 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | fluxerr "github.com/fluxcd/flux/pkg/errors" 8 | "github.com/fluxcd/flux/pkg/job" 9 | "github.com/fluxcd/flux/pkg/resource" 10 | ) 11 | 12 | type SyncErrors struct { 13 | errs map[resource.ID]error 14 | mu sync.Mutex 15 | } 16 | 17 | func manifestLoadError(reason error) error { 18 | return &fluxerr.Error{ 19 | Type: fluxerr.User, 20 | Err: reason, 21 | Help: `Unable to parse files as manifests 22 | 23 | Flux was unable to parse the files in the git repo as manifests, 24 | giving this error: 25 | 26 | ` + reason.Error() + ` 27 | 28 | Check that any files mentioned are well-formed, and resources are not 29 | defined more than once. It's also worth reviewing 30 | 31 | https://fluxcd.io/legacy/flux/requirements/ 32 | 33 | to make sure you're not running into any corner cases. 34 | 35 | If you think your files are all OK and you are still getting this 36 | message, please log an issue at 37 | 38 | https://github.com/fluxcd/flux/issues 39 | 40 | and include the problematic file, if possible. 41 | `, 42 | } 43 | } 44 | 45 | func unknownJobError(id job.ID) error { 46 | return &fluxerr.Error{ 47 | Type: fluxerr.Missing, 48 | Err: fmt.Errorf("unknown job %q", string(id)), 49 | Help: `Job not found 50 | 51 | This is often because the job did not result in committing changes, 52 | and therefore had no lasting effect. A release dry-run is an example 53 | of a job that does not result in a commit. 54 | 55 | If you were expecting changes to be committed, this may mean that the 56 | job failed, but its status was lost. 57 | 58 | In both of the above cases it is OK to retry the operation that 59 | resulted in this error. 60 | 61 | If you get this error repeatedly, it's probably a bug. Please log an 62 | issue describing what you were attempting, and posting logs from the 63 | daemon if possible: 64 | 65 | https://github.com/fluxcd/flux/issues 66 | 67 | `, 68 | } 69 | } 70 | 71 | func unsignedHeadRevisionError(latestValidRevision, headRevision string) error { 72 | return &fluxerr.Error{ 73 | Type: fluxerr.User, 74 | Err: fmt.Errorf("HEAD revision is unsigned"), 75 | Help: `HEAD is not a verified commit. 76 | 77 | The branch HEAD in the git repo is not verified, and fluxd is unable to 78 | make a change on top of it. The last verified commit was 79 | 80 | ` + latestValidRevision + ` 81 | 82 | HEAD is 83 | 84 | ` + headRevision + `. 85 | `, 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /pkg/daemon/metrics.go: -------------------------------------------------------------------------------- 1 | package daemon 2 | 3 | import ( 4 | "github.com/go-kit/kit/metrics/prometheus" 5 | stdprometheus "github.com/prometheus/client_golang/prometheus" 6 | 7 | fluxmetrics "github.com/fluxcd/flux/pkg/metrics" 8 | ) 9 | 10 | var ( 11 | // For us, syncs (of about 100 resources) take about thirty 12 | // seconds to a minute. Most short-lived (<1s) syncs will be failures. 13 | syncDuration = prometheus.NewHistogramFrom(stdprometheus.HistogramOpts{ 14 | Namespace: "flux", 15 | Subsystem: "daemon", 16 | Name: "sync_duration_seconds", 17 | Help: "Duration of git-to-cluster synchronisation, in seconds.", 18 | Buckets: []float64{0.5, 5, 10, 20, 30, 40, 50, 60, 75, 90, 120, 240}, 19 | }, []string{fluxmetrics.LabelSuccess}) 20 | 21 | // For most jobs, the majority of the time will be spent pushing 22 | // changes (git objects and refs) upstream. 23 | jobDuration = prometheus.NewHistogramFrom(stdprometheus.HistogramOpts{ 24 | Namespace: "flux", 25 | Subsystem: "daemon", 26 | Name: "job_duration_seconds", 27 | Help: "Duration of job execution, in seconds.", 28 | Buckets: []float64{0.1, 0.5, 1, 2, 5, 10, 15, 20, 30, 45, 60, 120}, 29 | }, []string{fluxmetrics.LabelSuccess}) 30 | 31 | // Same buckets as above (on the rough and ready assumption that 32 | // jobs will wait for some small multiple of job execution times) 33 | queueDuration = prometheus.NewHistogramFrom(stdprometheus.HistogramOpts{ 34 | Namespace: "flux", 35 | Subsystem: "daemon", 36 | Name: "queue_duration_seconds", 37 | Help: "Duration of time spent in the job queue before execution, in seconds.", 38 | Buckets: []float64{0.1, 0.5, 1, 2, 5, 10, 15, 20, 30, 45, 60, 120}, 39 | }, []string{}) 40 | 41 | queueLength = prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{ 42 | Namespace: "flux", 43 | Subsystem: "daemon", 44 | Name: "queue_length_count", 45 | Help: "Count of jobs waiting in the queue to be run.", 46 | }, []string{}) 47 | 48 | syncManifestsMetric = prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{ 49 | Namespace: "flux", 50 | Subsystem: "daemon", 51 | Name: "sync_manifests", 52 | Help: "Number of synchronized manifests", 53 | }, []string{fluxmetrics.LabelSuccess}) 54 | ) 55 | -------------------------------------------------------------------------------- /pkg/daemon/note.go: -------------------------------------------------------------------------------- 1 | package daemon 2 | 3 | import ( 4 | "github.com/fluxcd/flux/pkg/job" 5 | "github.com/fluxcd/flux/pkg/update" 6 | ) 7 | 8 | type note struct { 9 | JobID job.ID `json:"jobID"` 10 | Spec update.Spec `json:"spec"` 11 | Result update.Result `json:"result"` 12 | } 13 | -------------------------------------------------------------------------------- /pkg/errors/errors_test.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func TestZeroErrorEncoding(t *testing.T) { 11 | type S struct { 12 | Err *Error 13 | } 14 | var s S 15 | bytes, err := json.Marshal(s) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | var s1 S 20 | err = json.Unmarshal(bytes, &s1) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | if s1.Err != nil { 25 | t.Errorf("expected nil in field, but got %+v", s1.Err) 26 | } 27 | } 28 | 29 | func TestErrorEncoding(t *testing.T) { 30 | errVal := &Error{ 31 | Type: Server, 32 | Help: "helpful text\nwith linebreaks!", 33 | Err: errors.New("underlying error"), 34 | } 35 | bytes, err := json.Marshal(errVal) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | 40 | var got Error 41 | err = json.Unmarshal(bytes, &got) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | 46 | if got.Type != errVal.Type { 47 | println(string(bytes)) 48 | t.Errorf("error type: expected %q, got %q", errVal.Type, got.Type) 49 | } 50 | if got.Help != errVal.Help || got.Err.Error() != errVal.Err.Error() { 51 | t.Errorf("expected %+v\ngot %+v", errVal, got) 52 | } 53 | if !reflect.DeepEqual(errVal, &got) { 54 | t.Errorf("not deepEqual\nexpected %#v\ngot %#v", errVal, got) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /pkg/git/export.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | type Export struct { 10 | dir string 11 | } 12 | 13 | func (e *Export) Dir() string { 14 | return e.dir 15 | } 16 | 17 | func (e *Export) Clean() error { 18 | if e.dir != "" { 19 | return os.RemoveAll(e.dir) 20 | } 21 | return nil 22 | } 23 | 24 | // Export creates a minimal clone of the repo, at the ref given. 25 | func (r *Repo) Export(ctx context.Context, ref string) (*Export, error) { 26 | dir, err := r.workingClone(ctx, "") 27 | if err != nil { 28 | return nil, err 29 | } 30 | if err = checkout(ctx, dir, ref); err != nil { 31 | return nil, err 32 | } 33 | return &Export{dir}, nil 34 | } 35 | 36 | // SecretUnseal unseals git secrets in the clone. 37 | func (e *Export) SecretUnseal(ctx context.Context) error { 38 | return secretUnseal(ctx, e.Dir()) 39 | } 40 | 41 | // ChangedFiles does a git diff listing changed files 42 | func (e *Export) ChangedFiles(ctx context.Context, sinceRef string, paths []string) ([]string, error) { 43 | list, err := changed(ctx, e.Dir(), sinceRef, paths) 44 | if err == nil { 45 | for i, file := range list { 46 | list[i] = filepath.Join(e.Dir(), file) 47 | } 48 | } 49 | return list, err 50 | } 51 | -------------------------------------------------------------------------------- /pkg/git/export_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/fluxcd/flux/pkg/cluster/kubernetes/testfiles" 9 | ) 10 | 11 | func TestExportAtRevision(t *testing.T) { 12 | newDir, cleanup := testfiles.TempDir(t) 13 | defer cleanup() 14 | 15 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 16 | defer cancel() 17 | 18 | err := createRepo(newDir, []string{"config"}) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | repo := NewRepo(Remote{URL: newDir}, ReadOnly) 23 | if err := repo.Ready(ctx); err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | headMinusOne, err := repo.Revision(ctx, "HEAD^1") 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | 32 | export, err := repo.Export(ctx, headMinusOne) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | exportHead, err := refRevision(ctx, export.dir, "HEAD") 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | if headMinusOne != exportHead { 42 | t.Errorf("exported %s, but head in export dir %s is %s", headMinusOne, export.dir, exportHead) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pkg/git/metrics.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "github.com/go-kit/kit/metrics/prometheus" 5 | stdprometheus "github.com/prometheus/client_golang/prometheus" 6 | ) 7 | 8 | const ( 9 | MetricRepoReady = 1 10 | MetricRepoUnready = 0 11 | ) 12 | 13 | var ( 14 | metricGitReady = prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{ 15 | Namespace: "flux", 16 | Subsystem: "git", 17 | Name: "ready", 18 | Help: "Status of the git repository.", 19 | }, []string{}) 20 | ) 21 | -------------------------------------------------------------------------------- /pkg/git/signature.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | // Signature holds information about a GPG signature. 4 | type Signature struct { 5 | Key string 6 | Status string 7 | } 8 | 9 | // Valid returns true if the signature is _G_ood (valid). 10 | // https://github.com/git/git/blob/56d268bafff7538f82c01d3c9c07bdc54b2993b1/Documentation/pretty-formats.txt#L146-L153 11 | func (s *Signature) Valid() bool { 12 | return s.Status == "G" 13 | } 14 | -------------------------------------------------------------------------------- /pkg/git/url.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "strings" 7 | 8 | "github.com/whilp/git-urls" 9 | ) 10 | 11 | // Remote points at a git repo somewhere. 12 | type Remote struct { 13 | // URL is where we clone from 14 | URL string `json:"url"` 15 | } 16 | 17 | func (r Remote) SafeURL() string { 18 | u, err := giturls.Parse(r.URL) 19 | if err != nil { 20 | return fmt.Sprintf("", r.URL) 21 | } 22 | if u.User != nil { 23 | u.User = url.User(u.User.Username()) 24 | } 25 | return u.String() 26 | } 27 | 28 | // Equivalent compares the given URL with the remote URL without taking 29 | // protocols or `.git` suffixes into account. 30 | func (r Remote) Equivalent(u string) bool { 31 | lu, err := giturls.Parse(r.URL) 32 | if err != nil { 33 | return false 34 | } 35 | ru, err := giturls.Parse(u) 36 | if err != nil { 37 | return false 38 | } 39 | trimPath := func(p string) string { 40 | return strings.TrimSuffix(strings.TrimPrefix(p, "/"), ".git") 41 | } 42 | return lu.Host == ru.Host && trimPath(lu.Path) == trimPath(ru.Path) 43 | } 44 | -------------------------------------------------------------------------------- /pkg/git/url_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestSafeURL(t *testing.T) { 10 | const password = "abc123" 11 | for _, url := range []string{ 12 | "git@github.com:fluxcd/flux", 13 | "https://user@example.com:5050/repo.git", 14 | "https://user:" + password + "@example.com:5050/repo.git", 15 | } { 16 | u := Remote{url} 17 | if strings.Contains(u.SafeURL(), password) { 18 | t.Errorf("Safe URL for %s contains password %q", url, password) 19 | } 20 | } 21 | } 22 | 23 | func TestEquivalent(t *testing.T) { 24 | urls := []struct { 25 | remote string 26 | equivalent string 27 | equal bool 28 | }{ 29 | {"git@github.com:fluxcd/flux", "ssh://git@github.com/fluxcd/flux.git", true}, 30 | {"https://git@github.com/fluxcd/flux.git", "ssh://git@github.com/fluxcd/flux.git", true}, 31 | {"https://github.com/fluxcd/flux.git", "git@github.com:fluxcd/flux.git", true}, 32 | {"https://github.com/fluxcd/flux.git", "https://github.com/fluxcd/helm-operator.git", false}, 33 | } 34 | 35 | for _, u := range urls { 36 | r := Remote{u.remote} 37 | assert.Equal(t, u.equal, r.Equivalent(u.equivalent)) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /pkg/guid/guid.go: -------------------------------------------------------------------------------- 1 | package guid 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "time" 7 | ) 8 | 9 | func init() { 10 | rand.Seed(time.Now().UnixNano()) 11 | } 12 | 13 | func New() string { 14 | b := make([]byte, 16) 15 | rand.Read(b) 16 | return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) 17 | } 18 | -------------------------------------------------------------------------------- /pkg/http/accept.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | "sort" 6 | 7 | "github.com/golang/gddo/httputil/header" 8 | ) 9 | 10 | // negotiateContentType picks a content type based on the Accept 11 | // header from a request, and a supplied list of available content 12 | // types in order of preference. If the Accept header mentions more 13 | // than one available content type, the one with the highest quality 14 | // (`q`) parameter is chosen; if there are a number of those, the one 15 | // that appears first in the available types is chosen. 16 | func negotiateContentType(r *http.Request, orderedPref []string) string { 17 | specs := header.ParseAccept(r.Header, "Accept") 18 | if len(specs) == 0 { 19 | return orderedPref[0] 20 | } 21 | 22 | preferred := []header.AcceptSpec{} 23 | for _, spec := range specs { 24 | if indexOf(orderedPref, spec.Value) < len(orderedPref) { 25 | preferred = append(preferred, spec) 26 | } 27 | } 28 | if len(preferred) > 0 { 29 | sort.Sort(SortAccept{preferred, orderedPref}) 30 | return preferred[0].Value 31 | } 32 | return "" 33 | } 34 | 35 | type SortAccept struct { 36 | specs []header.AcceptSpec 37 | prefs []string 38 | } 39 | 40 | func (s SortAccept) Len() int { 41 | return len(s.specs) 42 | } 43 | 44 | // We want to sort by descending order of suitability: higher quality 45 | // to lower quality, and preferred to less preferred. 46 | func (s SortAccept) Less(i, j int) bool { 47 | switch { 48 | case s.specs[i].Q == s.specs[j].Q: 49 | return indexOf(s.prefs, s.specs[i].Value) < indexOf(s.prefs, s.specs[j].Value) 50 | default: 51 | return s.specs[i].Q > s.specs[j].Q 52 | } 53 | } 54 | 55 | func (s SortAccept) Swap(i, j int) { 56 | s.specs[i], s.specs[j] = s.specs[j], s.specs[i] 57 | } 58 | 59 | // This exists so we can search short slices of strings without 60 | // requiring them to be sorted. Returning the len value if not found 61 | // is so that it can be used directly in a comparison when sorting (a 62 | // `-1` would mean "not found" was sorted before found entries). 63 | func indexOf(ss []string, search string) int { 64 | for i, s := range ss { 65 | if s == search { 66 | return i 67 | } 68 | } 69 | return len(ss) 70 | } 71 | -------------------------------------------------------------------------------- /pkg/http/accept_test.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | ) 7 | 8 | func Test_NegotiateContentType(t *testing.T) { 9 | // For no accept header, you get your first choice 10 | want := "x-world/x-vrml" 11 | got := negotiateContentType(&http.Request{}, []string{want}) 12 | if got != want { 13 | t.Errorf("First choice: Expected %q, got %q", want, got) 14 | } 15 | 16 | // If there's accept headers but none match, get "" 17 | h := http.Header{} 18 | h.Add("Accept", "application/json;q=1.0,text/html;q=0.9") 19 | h.Add("Accept", "text/plain") 20 | got = negotiateContentType(&http.Request{Header: h}, []string{want}) 21 | if got != "" { 22 | t.Errorf("No matching: expected empty string, got %q", got) 23 | } 24 | 25 | // If there's accept headers that match, of equal quality (`q`), 26 | // return the first preference. 27 | h = http.Header{} 28 | h.Add("Accept", "application/json,x-world/x-vrml,text/html") 29 | got = negotiateContentType(&http.Request{Header: h}, []string{want, "application/json"}) 30 | if got != want { 31 | t.Errorf("Equal quality: expected %q, got %q", want, got) 32 | } 33 | 34 | // If there's matching accept headers of different quality, pick 35 | // the highest quality match even if it's not first preference. 36 | h = http.Header{} 37 | h.Add("Accept", "application/json;q=0.5,text/html;q=1.0") 38 | got = negotiateContentType(&http.Request{Header: h}, []string{"application/json", "text/html"}) 39 | if got != "text/html" { 40 | t.Errorf("Quality beats preference: expected %q, got %q", "text/html", got) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pkg/http/daemon/server_test.go: -------------------------------------------------------------------------------- 1 | package daemon 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/fluxcd/flux/pkg/http" 7 | ) 8 | 9 | func TestRouterImplementsServer(t *testing.T) { 10 | router := NewRouter() 11 | // Calling NewHandler attaches handlers to the router 12 | NewHandler(nil, router) 13 | err := http.ImplementsServer(router) 14 | if err != nil { 15 | t.Error(err) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /pkg/http/daemon/upstream_test.go: -------------------------------------------------------------------------------- 1 | package daemon 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestEndpointInference(t *testing.T) { 8 | wsEndpoint := "ws://cloud.weave.works/api/flux" 9 | wssEndpoint := "wss://cloud.weave.works/api/flux" 10 | httpEndpoint := "http://cloud.weave.works/api/flux" 11 | httpsEndpoint := "https://cloud.weave.works/api/flux" 12 | 13 | assertExpected(t, wsEndpoint, httpEndpoint, wsEndpoint) 14 | assertExpected(t, wssEndpoint, httpsEndpoint, wssEndpoint) 15 | assertExpected(t, httpEndpoint, httpEndpoint, wsEndpoint) 16 | assertExpected(t, httpsEndpoint, httpsEndpoint, wssEndpoint) 17 | } 18 | 19 | func assertExpected(t *testing.T, input, expectedHTTP, expectedWS string) { 20 | actualHTTP, actualWS, err := inferEndpoints(input) 21 | if err != nil { 22 | t.Error(err) 23 | } 24 | assertEquals(t, expectedHTTP, actualHTTP) 25 | assertEquals(t, expectedWS, actualWS) 26 | } 27 | 28 | func assertEquals(t *testing.T, expected, actual string) { 29 | if expected != actual { 30 | t.Errorf("Expected [%s], actual [%s]", expected, actual) 31 | } 32 | } 33 | 34 | func TestUnsupportedEndpoint(t *testing.T) { 35 | _, _, err := inferEndpoints("mailto://cloud.weave.works/api/flux") 36 | if err == nil { 37 | t.Error("Expected err, got nil") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /pkg/http/errors.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "errors" 5 | 6 | fluxerr "github.com/fluxcd/flux/pkg/errors" 7 | ) 8 | 9 | var ErrorDeprecated = &fluxerr.Error{ 10 | Type: fluxerr.Missing, 11 | Help: `The API endpoint requested appears to have been deprecated. 12 | 13 | This indicates your client (fluxctl) needs to be updated: please see 14 | 15 | https://github.com/fluxcd/flux/releases 16 | 17 | If you still have this problem after upgrading, please file an issue at 18 | 19 | https://github.com/fluxcd/flux/issues 20 | 21 | mentioning what you were attempting to do. 22 | `, 23 | Err: errors.New("API endpoint deprecated"), 24 | } 25 | 26 | var ErrorUnauthorized = &fluxerr.Error{ 27 | Type: fluxerr.User, 28 | Help: `The request failed authentication 29 | 30 | This most likely means you have a missing or incorrect token. Please 31 | make sure you supply a service token, either by setting the 32 | environment variable FLUX_SERVICE_TOKEN, or using the argument --token 33 | with fluxctl. 34 | 35 | `, 36 | Err: errors.New("request failed authentication"), 37 | } 38 | 39 | func MakeAPINotFound(path string) *fluxerr.Error { 40 | return &fluxerr.Error{ 41 | Type: fluxerr.Missing, 42 | Help: `The API endpoint requested is not supported by this server. 43 | 44 | This indicates that your client (probably fluxctl) is either out of 45 | date, or faulty. Please see 46 | 47 | https://github.com/fluxcd/flux/releases 48 | 49 | for releases of fluxctl. 50 | 51 | If you still have problems, please file an issue at 52 | 53 | https://github.com/fluxcd/flux/issues 54 | 55 | mentioning what you were attempting to do, and include this path: 56 | 57 | ` + path + ` 58 | `, 59 | Err: errors.New("API endpoint not found"), 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /pkg/http/httperror/api_error.go: -------------------------------------------------------------------------------- 1 | package httperror 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | // When an API call fails, we may want to distinguish among the causes 9 | // by status code. This type can be used as the base error when we get 10 | // a non-"HTTP 20x" response, retrievable with errors.Cause(err). 11 | type APIError struct { 12 | StatusCode int 13 | Status string 14 | Body string 15 | } 16 | 17 | func (err *APIError) Error() string { 18 | return fmt.Sprintf("%s (%s)", err.Status, err.Body) 19 | } 20 | 21 | // Does this error mean the API service is unavailable? 22 | func (err *APIError) IsUnavailable() bool { 23 | switch err.StatusCode { 24 | case 502, 503, 504: 25 | return true 26 | } 27 | return false 28 | } 29 | 30 | // Is this API call missing? This usually indicates that there is a 31 | // version mismatch between the client and the service. 32 | func (err *APIError) IsMissing() bool { 33 | return err.StatusCode == http.StatusNotFound 34 | } 35 | -------------------------------------------------------------------------------- /pkg/http/routes.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | const ( 4 | // Formerly Upstream methods, now (in v11) included in server API 5 | Ping = "Ping" 6 | Version = "Version" 7 | Notify = "Notify" 8 | 9 | ListServices = "ListServices" 10 | ListServicesWithOptions = "ListServicesWithOptions" 11 | ListImages = "ListImages" 12 | ListImagesWithOptions = "ListImagesWithOptions" 13 | UpdateManifests = "UpdateManifests" 14 | JobStatus = "JobStatus" 15 | SyncStatus = "SyncStatus" 16 | Export = "Export" 17 | GitRepoConfig = "GitRepoConfig" 18 | 19 | UpdateImages = "UpdateImages" 20 | UpdatePolicies = "UpdatePolicies" 21 | GetPublicSSHKey = "GetPublicSSHKey" 22 | RegeneratePublicSSHKey = "RegeneratePublicSSHKey" 23 | ) 24 | 25 | // This is part of the API -- but it's the outward-facing (or service 26 | // provider) API, rather than the flux API. 27 | const ( 28 | LogEvent = "LogEvent" 29 | ) 30 | 31 | // The RegisterDaemonX routes should move to weaveworks/flux-adapter 32 | // once we remove `--connect`, since they are pertinent only to making 33 | // an RPC relay connection. 34 | const ( 35 | RegisterDaemonV6 = "RegisterDaemonV6" 36 | RegisterDaemonV7 = "RegisterDaemonV7" 37 | RegisterDaemonV8 = "RegisterDaemonV8" 38 | RegisterDaemonV9 = "RegisterDaemonV9" 39 | RegisterDaemonV10 = "RegisterDaemonV10" 40 | RegisterDaemonV11 = "RegisterDaemonV11" 41 | ) 42 | -------------------------------------------------------------------------------- /pkg/http/validate.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gorilla/mux" 7 | ) 8 | 9 | // ImplementsServer verifies that a given `*mux.Router` has handlers for 10 | // all routes specified in `NewAPIRouter()`. 11 | // 12 | // We can't easily check whether a router implements the `api.Server` 13 | // interface, as would be desired, so we rely on the knowledge that 14 | // `*client.Client` implements `api.Server` while also depending on 15 | // route name strings defined in this package. 16 | // 17 | // Returns an error if router doesn't fully implement `NewAPIRouter()`, 18 | // nil otherwise. 19 | func ImplementsServer(router *mux.Router) error { 20 | apiRouter := NewAPIRouter() 21 | return apiRouter.Walk(makeWalkFunc(router)) 22 | } 23 | 24 | // makeWalkFunc creates a function which verifies that the route passed 25 | // to it both exists in the router under test and has a handler attached. 26 | func makeWalkFunc(router *mux.Router) mux.WalkFunc { 27 | return mux.WalkFunc(func(r *mux.Route, _ *mux.Router, _ []*mux.Route) error { 28 | // Does a route with this name exist in router? 29 | route := router.Get(r.GetName()) 30 | if route == nil { 31 | return fmt.Errorf("no route by name %q in router", r.GetName()) 32 | } 33 | // Does the route have a handler? 34 | handler := route.GetHandler() 35 | if handler == nil { 36 | return fmt.Errorf("no handler for route %q in router", r.GetName()) 37 | } 38 | return nil 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /pkg/http/websocket/client.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/http" 7 | "net/url" 8 | 9 | "github.com/gorilla/websocket" 10 | "github.com/pkg/errors" 11 | 12 | "github.com/fluxcd/flux/pkg/http/client" 13 | ) 14 | 15 | type DialErr struct { 16 | URL *url.URL 17 | HTTPResponse *http.Response 18 | } 19 | 20 | func (de DialErr) Error() string { 21 | if de.URL != nil && de.HTTPResponse != nil { 22 | return fmt.Sprintf("connecting to websocket %s (http status code = %v)", de.URL, de.HTTPResponse.StatusCode) 23 | } 24 | return "connecting to websocket (unknown error)" 25 | } 26 | 27 | // Dial initiates a new websocket connection. 28 | func Dial(client *http.Client, ua string, token client.Token, u *url.URL) (Websocket, error) { 29 | // Build the http request 30 | req, err := http.NewRequest("GET", u.String(), nil) 31 | if err != nil { 32 | return nil, errors.Wrapf(err, "constructing request %s", u) 33 | } 34 | 35 | // Send version in user-agent 36 | req.Header.Set("User-Agent", ua) 37 | 38 | // Add authentication if provided 39 | token.Set(req) 40 | 41 | // Use http client to do the http request 42 | conn, resp, err := dialer(client).Dial(u.String(), req.Header) 43 | if err != nil { 44 | if resp != nil { 45 | err = &DialErr{u, resp} 46 | } 47 | return nil, err 48 | } 49 | 50 | // Set up the ping heartbeat 51 | return Ping(conn), nil 52 | } 53 | 54 | func dialer(client *http.Client) *websocket.Dialer { 55 | return &websocket.Dialer{ 56 | NetDial: func(network, addr string) (net.Conn, error) { 57 | return net.DialTimeout(network, addr, client.Timeout) 58 | }, 59 | HandshakeTimeout: client.Timeout, 60 | Jar: client.Jar, 61 | // TODO: TLSClientConfig: client.TLSClientConfig, 62 | // TODO: Proxy 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /pkg/http/websocket/server.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gorilla/websocket" 7 | ) 8 | 9 | var upgrader = websocket.Upgrader{ 10 | CheckOrigin: func(r *http.Request) bool { return true }, 11 | } 12 | 13 | // Upgrade upgrades the HTTP server connection to the WebSocket protocol. 14 | func Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (Websocket, error) { 15 | wsConn, err := upgrader.Upgrade(w, r, responseHeader) 16 | if err != nil { 17 | return nil, err 18 | } 19 | return Ping(wsConn), nil 20 | } 21 | -------------------------------------------------------------------------------- /pkg/http/websocket/websocket.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | // This package can be moved to weaveworks/flux-adapter once 4 | // `--connect` is removed, since it is particular to making an RPC 5 | // relay connection, and that function will be supplied by 6 | // flux-adapter. 7 | 8 | import ( 9 | "io" 10 | 11 | "github.com/gorilla/websocket" 12 | ) 13 | 14 | // Websocket exposes the bits of *websocket.Conn we actually use. Note 15 | // that we are emulating an `io.ReadWriter`. This is to be able 16 | // to support RPC codecs, which operate on byte streams. 17 | type Websocket interface { 18 | io.Reader 19 | io.Writer 20 | Close() error 21 | } 22 | 23 | // IsExpectedWSCloseError returns boolean indicating whether the error is a 24 | // clean disconnection. 25 | func IsExpectedWSCloseError(err error) bool { 26 | return err == io.EOF || err == io.ErrClosedPipe || websocket.IsCloseError(err, 27 | websocket.CloseNormalClosure, 28 | websocket.CloseGoingAway, 29 | websocket.CloseNoStatusReceived, 30 | websocket.CloseAbnormalClosure, 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /pkg/http/websocket/websocket_test.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net/http" 7 | "net/http/httptest" 8 | "net/url" 9 | "sync" 10 | "testing" 11 | 12 | "github.com/fluxcd/flux/pkg/http/client" 13 | ) 14 | 15 | func TestToken(t *testing.T) { 16 | token := "toooookkkkkeeeeennnnnn" 17 | upgrade := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 18 | tok := r.Header.Get("Authorization") 19 | if tok != "Scope-Probe token="+token { 20 | t.Fatal("Did not get authorisation header, got: " + tok) 21 | } 22 | _, err := Upgrade(w, r, nil) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | }) 27 | 28 | srv := httptest.NewServer(upgrade) 29 | defer srv.Close() 30 | 31 | url, _ := url.Parse(srv.URL) 32 | url.Scheme = "ws" 33 | 34 | ws, err := Dial(http.DefaultClient, "fluxd/test", client.Token(token), url) 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | defer ws.Close() 39 | } 40 | 41 | func TestByteStream(t *testing.T) { 42 | buf := &bytes.Buffer{} 43 | var wg sync.WaitGroup 44 | wg.Add(1) 45 | upgrade := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 46 | ws, err := Upgrade(w, r, nil) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | if _, err := io.Copy(buf, ws); err != nil { 51 | t.Fatal(err) 52 | } 53 | wg.Done() 54 | }) 55 | 56 | srv := httptest.NewServer(upgrade) 57 | 58 | url, _ := url.Parse(srv.URL) 59 | url.Scheme = "ws" 60 | 61 | ws, err := Dial(http.DefaultClient, "fluxd/test", client.Token(""), url) 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | 66 | checkWrite := func(msg string) { 67 | if _, err := ws.Write([]byte(msg)); err != nil { 68 | t.Fatal(err) 69 | } 70 | } 71 | 72 | checkWrite("hey") 73 | checkWrite(" there") 74 | checkWrite(" champ") 75 | if err := ws.Close(); err != nil { 76 | t.Fatal(err) 77 | } 78 | 79 | // Make sure the server reads everything from the connection 80 | srv.Close() 81 | wg.Wait() 82 | if buf.String() != "hey there champ" { 83 | t.Fatalf("did not collect message as expected, got %s", buf.String()) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /pkg/install/generate.go: -------------------------------------------------------------------------------- 1 | // +build tools 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "os" 10 | "time" 11 | 12 | "github.com/shurcooL/vfsgen" 13 | ) 14 | 15 | func main() { 16 | usage := func() { 17 | fmt.Fprintf(os.Stderr, "usage: %s\n", os.Args[0]) 18 | os.Exit(1) 19 | } 20 | if len(os.Args) != 1 { 21 | usage() 22 | } 23 | 24 | var fs http.FileSystem = modTimeFS{ 25 | fs: http.Dir("templates/"), 26 | } 27 | err := vfsgen.Generate(fs, vfsgen.Options{ 28 | Filename: "generated_templates.gogen.go", 29 | PackageName: "install", 30 | VariableName: "templates", 31 | }) 32 | if err != nil { 33 | log.Fatalln(err) 34 | } 35 | } 36 | 37 | // modTimeFS is a wrapper that rewrites all mod times to Unix epoch. 38 | // This is to ensure `generated_templates.gogen.go` only changes when 39 | // the folder and/or file contents change. 40 | type modTimeFS struct { 41 | fs http.FileSystem 42 | } 43 | 44 | func (fs modTimeFS) Open(name string) (http.File, error) { 45 | f, err := fs.fs.Open(name) 46 | if err != nil { 47 | return nil, err 48 | } 49 | return modTimeFile{f}, nil 50 | } 51 | 52 | type modTimeFile struct { 53 | http.File 54 | } 55 | 56 | func (f modTimeFile) Stat() (os.FileInfo, error) { 57 | fi, err := f.File.Stat() 58 | if err != nil { 59 | return nil, err 60 | } 61 | return modTimeFileInfo{fi}, nil 62 | } 63 | 64 | type modTimeFileInfo struct { 65 | os.FileInfo 66 | } 67 | 68 | func (modTimeFileInfo) ModTime() time.Time { 69 | return time.Unix(0, 0) 70 | } 71 | -------------------------------------------------------------------------------- /pkg/install/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fluxcd/flux/pkg/install 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/instrumenta/kubeval v0.16.1 7 | github.com/kr/pretty v0.1.0 // indirect 8 | github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 9 | github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 10 | github.com/spf13/pflag v1.0.5 // indirect 11 | github.com/stretchr/testify v1.7.1 12 | golang.org/x/tools v0.0.0-20200121210457-b3205ff6fffe // indirect 13 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 14 | gopkg.in/yaml.v2 v2.2.8 15 | ) 16 | -------------------------------------------------------------------------------- /pkg/install/install.go: -------------------------------------------------------------------------------- 1 | package install 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "strings" 10 | "text/template" 11 | 12 | "github.com/shurcooL/httpfs/vfsutil" 13 | ) 14 | 15 | //go:generate go run generate.go 16 | 17 | type TemplateParameters struct { 18 | GitURL string 19 | GitBranch string 20 | GitPaths []string 21 | GitLabel string 22 | GitUser string 23 | GitEmail string 24 | GitReadOnly bool 25 | RegistryDisableScanning bool 26 | Namespace string 27 | ManifestGeneration bool 28 | AdditionalFluxArgs []string 29 | AddSecurityContext bool 30 | } 31 | 32 | func FillInTemplates(params TemplateParameters) (map[string][]byte, error) { 33 | result := map[string][]byte{} 34 | err := vfsutil.WalkFiles(templates, "/", func(path string, info os.FileInfo, rs io.ReadSeeker, err error) error { 35 | if err != nil { 36 | return fmt.Errorf("cannot walk embedded files: %s", err) 37 | } 38 | if info.IsDir() { 39 | return nil 40 | } 41 | if params.RegistryDisableScanning && strings.Contains(info.Name(), "memcache") { 42 | // do not include memcached resources when registry scanning is disabled 43 | return nil 44 | } 45 | manifestTemplateBytes, err := ioutil.ReadAll(rs) 46 | if err != nil { 47 | return fmt.Errorf("cannot read embedded file %q: %s", info.Name(), err) 48 | } 49 | manifestTemplate, err := template.New(info.Name()). 50 | Funcs(template.FuncMap{"StringsJoin": strings.Join}). 51 | Parse(string(manifestTemplateBytes)) 52 | if err != nil { 53 | return fmt.Errorf("cannot parse embedded file %q: %s", info.Name(), err) 54 | } 55 | out := bytes.NewBuffer(nil) 56 | if err := manifestTemplate.Execute(out, params); err != nil { 57 | return fmt.Errorf("cannot execute template for embedded file %q: %s", info.Name(), err) 58 | } 59 | if out.Len() > 0 { 60 | result[strings.TrimSuffix(info.Name(), ".tmpl")] = out.Bytes() 61 | } 62 | return nil 63 | }) 64 | if err != nil { 65 | return nil, fmt.Errorf("internal error filling embedded installation templates: %s", err) 66 | } 67 | return result, nil 68 | } 69 | -------------------------------------------------------------------------------- /pkg/install/templates/flux-account.yaml.tmpl: -------------------------------------------------------------------------------- 1 | --- 2 | # The service account, cluster roles, and cluster role binding are 3 | # only needed for Kubernetes with role-based access control (RBAC). 4 | apiVersion: v1 5 | kind: ServiceAccount 6 | metadata: 7 | labels: 8 | name: flux 9 | name: flux{{ if .Namespace }} 10 | namespace: {{ .Namespace }}{{ end}} 11 | --- 12 | apiVersion: rbac.authorization.k8s.io/v1 13 | kind: ClusterRole 14 | metadata: 15 | labels: 16 | name: flux 17 | name: flux 18 | rules: 19 | - apiGroups: ['*'] 20 | resources: ['*'] 21 | verbs: ['*'] 22 | - nonResourceURLs: ['*'] 23 | verbs: ['*'] 24 | --- 25 | apiVersion: rbac.authorization.k8s.io/v1 26 | kind: ClusterRoleBinding 27 | metadata: 28 | labels: 29 | name: flux 30 | name: flux 31 | roleRef: 32 | apiGroup: rbac.authorization.k8s.io 33 | kind: ClusterRole 34 | name: flux 35 | subjects: 36 | - kind: ServiceAccount 37 | name: flux 38 | namespace: {{ if .Namespace }}{{ .Namespace }}{{ else }}default{{ end }} 39 | -------------------------------------------------------------------------------- /pkg/install/templates/flux-secret.yaml.tmpl: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: flux-git-deploy{{ if .Namespace }} 6 | namespace: {{ .Namespace }}{{ end }} 7 | type: Opaque 8 | -------------------------------------------------------------------------------- /pkg/install/templates/memcache-dep.yaml.tmpl: -------------------------------------------------------------------------------- 1 | --- 2 | # memcached deployment used by Flux to cache 3 | # container image metadata. 4 | apiVersion: apps/v1 5 | kind: Deployment 6 | metadata: 7 | name: memcached{{ if .Namespace }} 8 | namespace: {{ .Namespace }}{{ end }} 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | name: memcached 14 | template: 15 | metadata: 16 | labels: 17 | name: memcached 18 | spec: 19 | nodeSelector: 20 | beta.kubernetes.io/os: linux 21 | containers: 22 | - name: memcached 23 | image: memcached:1.6.10-alpine 24 | imagePullPolicy: IfNotPresent 25 | args: 26 | - -m 512 # Maximum memory to use, in megabytes 27 | - -I 5m # Maximum size for one item 28 | - -p 11211 # Default port 29 | # - -vv # Uncomment to get logs of each request and response. 30 | ports: 31 | - name: clients 32 | containerPort: 11211{{ if .AddSecurityContext}} 33 | securityContext: 34 | runAsUser: 11211 35 | runAsGroup: 11211 36 | allowPrivilegeEscalation: false{{ end }} 37 | -------------------------------------------------------------------------------- /pkg/install/templates/memcache-svc.yaml.tmpl: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: memcached{{ if .Namespace }} 6 | namespace: {{ .Namespace }}{{ end }} 7 | spec: 8 | ports: 9 | - name: memcached 10 | port: 11211 11 | selector: 12 | name: memcached 13 | -------------------------------------------------------------------------------- /pkg/job/job_test.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | ) 7 | 8 | func TestQueue(t *testing.T) { 9 | shutdown := make(chan struct{}) 10 | wg := &sync.WaitGroup{} 11 | defer close(shutdown) 12 | q := NewQueue(shutdown, wg) 13 | if q.Len() != 0 { 14 | t.Errorf("Fresh queue has length %d (!= 0)", q.Len()) 15 | } 16 | 17 | select { 18 | case <-q.Ready(): 19 | t.Error("Value from q.Ready before any values enqueued") 20 | default: 21 | } 22 | 23 | // When this proceeds, the value will be in the queue 24 | q.Enqueue(&Job{"job 1", nil}) 25 | q.Sync() 26 | if q.Len() != 1 { 27 | t.Errorf("Queue has length %d (!= 1) after enqueuing one item (and sync)", q.Len()) 28 | } 29 | 30 | // This should proceed eventually 31 | j := <-q.Ready() 32 | if j.ID != "job 1" { 33 | t.Errorf("Dequeued odd job: %#v", j) 34 | } 35 | q.Sync() 36 | if q.Len() != 0 { 37 | t.Errorf("Queue has length %d (!= 0) after dequeuing only item (and sync)", q.Len()) 38 | } 39 | 40 | // This should not proceed, because the queue is empty 41 | select { 42 | case j = <-q.Ready(): 43 | t.Errorf("Dequeued from empty queue: %#v", j) 44 | default: 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /pkg/job/status_cache.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | type StatusCache struct { 8 | // Size is the number of statuses to store. When full, jobs are evicted in FIFO ordering. 9 | // oldest ones will be evicted to make room. 10 | Size int 11 | 12 | // Store cache entries in an array to make fifo eviction easier. Efficiency 13 | // doesn't matter because the cache is small and computers are fast. 14 | cache []cacheEntry 15 | sync.RWMutex 16 | } 17 | 18 | type cacheEntry struct { 19 | ID ID 20 | Status Status 21 | } 22 | 23 | func (c *StatusCache) SetStatus(id ID, status Status) { 24 | if c.Size <= 0 { 25 | return 26 | } 27 | c.Lock() 28 | defer c.Unlock() 29 | if i := c.statusIndex(id); i >= 0 { 30 | // already exists, update 31 | c.cache[i].Status = status 32 | } 33 | // Evict, if we need to. Eviction is done first, so that append can only copy 34 | // the things we care about keeping. Micro-optimize to the max. 35 | if c.Size <= len(c.cache) { 36 | c.cache = c.cache[len(c.cache)-(c.Size-1):] 37 | } 38 | c.cache = append(c.cache, cacheEntry{ 39 | ID: id, 40 | Status: status, 41 | }) 42 | } 43 | 44 | func (c *StatusCache) Status(id ID) (Status, bool) { 45 | c.RLock() 46 | defer c.RUnlock() 47 | i := c.statusIndex(id) 48 | if i < 0 { 49 | return Status{}, false 50 | } 51 | return c.cache[i].Status, true 52 | } 53 | 54 | func (c *StatusCache) statusIndex(id ID) int { 55 | // entries are sorted by arrival time, not id, so we can't use binary search. 56 | for i := range c.cache { 57 | if c.cache[i].ID == id { 58 | return i 59 | } 60 | } 61 | return -1 62 | } 63 | -------------------------------------------------------------------------------- /pkg/manifests/manifests.go: -------------------------------------------------------------------------------- 1 | package manifests 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/fluxcd/flux/pkg/image" 7 | "github.com/fluxcd/flux/pkg/resource" 8 | ) 9 | 10 | // Manifests represents a format for files or chunks of bytes 11 | // containing definitions of resources, e.g., in Kubernetes, YAML 12 | // files defining Kubernetes resources. 13 | type Manifests interface { 14 | // Load all the resource manifests under the paths 15 | // given. `baseDir` is used to relativise the paths, which are 16 | // supplied as absolute paths to directories or files; at least 17 | // one path should be supplied, even if it is the same as `baseDir`. 18 | LoadManifests(baseDir string, paths []string) (map[string]resource.Resource, error) 19 | // ParseManifest parses the content of a collection of manifests, into resources 20 | ParseManifest(def []byte, source string) (map[string]resource.Resource, error) 21 | // Set the image of a container in a manifest's bytes to that given 22 | SetWorkloadContainerImage(def []byte, resourceID resource.ID, container string, newImageID image.Ref) ([]byte, error) 23 | // UpdateWorkloadPolicies modifies a manifest to apply the policy update specified 24 | UpdateWorkloadPolicies(def []byte, id resource.ID, update resource.PolicyUpdate) ([]byte, error) 25 | // CreateManifestPatch obtains a patch between the original and modified manifests 26 | CreateManifestPatch(originalManifests, modifiedManifests []byte, originalSource, modifiedSource string) ([]byte, error) 27 | // ApplyManifestPatch applies a manifest patch (obtained with CreateManifestPatch) returning the patched manifests 28 | ApplyManifestPatch(originalManifests, patchManifests []byte, originalSource, patchSource string) ([]byte, error) 29 | // AppendManifestToBuffer concatenates manifest bytes to a 30 | // (possibly empty) buffer of manifest bytes; the resulting bytes 31 | // should be parsable by `ParseManifest`. 32 | // TODO(michael) should really be an interface rather than `*bytes.Buffer`. 33 | AppendManifestToBuffer(manifest []byte, buffer *bytes.Buffer) error 34 | } 35 | -------------------------------------------------------------------------------- /pkg/manifests/store.go: -------------------------------------------------------------------------------- 1 | package manifests 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/fluxcd/flux/pkg/image" 8 | "github.com/fluxcd/flux/pkg/resource" 9 | ) 10 | 11 | type StoreError struct { 12 | error 13 | } 14 | 15 | func ErrResourceNotFound(name string) error { 16 | return StoreError{fmt.Errorf("resource %s not found", name)} 17 | } 18 | 19 | // Store manages all the cluster resources defined in a checked out repository, explicitly declared 20 | // in a file or not e.g., generated and updated by a .flux.yaml file, explicit Kubernetes .yaml manifests files ... 21 | type Store interface { 22 | // Set the container image of a resource in the store 23 | SetWorkloadContainerImage(ctx context.Context, resourceID resource.ID, container string, newImageID image.Ref) error 24 | // UpdateWorkloadPolicies modifies a resource in the store to apply the policy-update specified. 25 | // It returns whether a change in the resource was actually made as a result of the change 26 | UpdateWorkloadPolicies(ctx context.Context, resourceID resource.ID, update resource.PolicyUpdate) (bool, error) 27 | // Load all the resources in the store. The returned map is indexed by the resource IDs 28 | GetAllResourcesByID(ctx context.Context) (map[string]resource.Resource, error) 29 | } 30 | -------------------------------------------------------------------------------- /pkg/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | /* 4 | Labels and so on for metrics used in Flux. 5 | */ 6 | 7 | const ( 8 | LabelRoute = "route" 9 | LabelMethod = "method" 10 | LabelSuccess = "success" 11 | 12 | // Labels for release metrics 13 | LabelAction = "action" 14 | LabelReleaseType = "release_type" 15 | LabelReleaseKind = "release_kind" 16 | LabelStage = "stage" 17 | ) 18 | -------------------------------------------------------------------------------- /pkg/policy/policy_test.go: -------------------------------------------------------------------------------- 1 | package policy 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestJSON(t *testing.T) { 13 | boolPolicy := Set{} 14 | boolPolicy = boolPolicy.Add(Ignore) 15 | boolPolicy = boolPolicy.Add(Locked) 16 | policy := boolPolicy.Set(LockedUser, "user@example.com") 17 | 18 | if !(policy.Has(Ignore) && policy.Has(Locked)) { 19 | t.Errorf("Policies did not include those added") 20 | } 21 | if val, ok := policy.Get(LockedUser); !ok || val != "user@example.com" { 22 | t.Errorf("Policies did not include policy that was set") 23 | } 24 | 25 | bs, err := json.Marshal(policy) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | 30 | var policy2 Set 31 | if err = json.Unmarshal(bs, &policy2); err != nil { 32 | t.Fatal(err) 33 | } 34 | if !reflect.DeepEqual(policy, policy2) { 35 | t.Errorf("Roundtrip did not preserve policy. Expected:\n%#v\nGot:\n%#v\n", policy, policy2) 36 | } 37 | 38 | listyPols := []Policy{Ignore, Locked} 39 | bs, err = json.Marshal(listyPols) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | policy2 = Set{} 44 | if err = json.Unmarshal(bs, &policy2); err != nil { 45 | t.Fatal(err) 46 | } 47 | if !reflect.DeepEqual(boolPolicy, policy2) { 48 | t.Errorf("Parsing equivalent list did not preserve policy. Expected:\n%#v\nGot:\n%#v\n", policy, policy2) 49 | } 50 | } 51 | 52 | func Test_GetTagPattern(t *testing.T) { 53 | container := "helloContainer" 54 | 55 | type args struct { 56 | policies Set 57 | container string 58 | } 59 | tests := []struct { 60 | name string 61 | args args 62 | want Pattern 63 | }{ 64 | { 65 | name: "Nil policies", 66 | args: args{policies: nil}, 67 | want: PatternAll, 68 | }, 69 | { 70 | name: "No match", 71 | args: args{policies: Set{}}, 72 | want: PatternAll, 73 | }, 74 | { 75 | name: "Match", 76 | args: args{ 77 | policies: Set{ 78 | Policy(fmt.Sprintf("tag.%s", container)): "glob:master-*", 79 | }, 80 | container: container, 81 | }, 82 | want: NewPattern("master-*"), 83 | }, 84 | } 85 | for _, tt := range tests { 86 | t.Run(tt.name, func(t *testing.T) { 87 | assert.Equal(t, tt.want, GetTagPattern(tt.args.policies, tt.args.container)) 88 | }) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /pkg/registry/azure.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "strings" 7 | ) 8 | 9 | const ( 10 | // Mount volume from hostpath. 11 | azureCloudConfigJsonFile = "/etc/kubernetes/azure.json" 12 | ) 13 | 14 | type azureCloudConfig struct { 15 | AADClientId string `json:"aadClientId"` 16 | AADClientSecret string `json:"aadClientSecret"` 17 | } 18 | 19 | // Fetch Azure Active Directory clientid/secret pair from azure.json, usable for container registry authentication. 20 | // 21 | // Note: azure.json is populated by AKS/AKS-Engine script kubernetesconfigs.sh. The file is then passed to kubelet via 22 | // --azure-container-registry-config=/etc/kubernetes/azure.json, parsed by kubernetes/kubernetes' azure_credentials.go 23 | // https://github.com/kubernetes/kubernetes/issues/58034 seeks to deprecate this kubelet command-line argument, possibly 24 | // replacing it with managed identity for the Node VMs. See https://github.com/Azure/acr/blob/master/docs/AAD-OAuth.md 25 | func getAzureCloudConfigAADToken(host string) (creds, error) { 26 | jsonFile, err := ioutil.ReadFile(azureCloudConfigJsonFile) 27 | if err != nil { 28 | return creds{}, err 29 | } 30 | 31 | var token azureCloudConfig 32 | 33 | err = json.Unmarshal(jsonFile, &token) 34 | if err != nil { 35 | return creds{}, err 36 | } 37 | 38 | return creds{ 39 | registry: host, 40 | provenance: "azure.json", 41 | username: token.AADClientId, 42 | password: token.AADClientSecret}, nil 43 | } 44 | 45 | // List from https://github.com/kubernetes/kubernetes/blob/master/pkg/credentialprovider/azure/azure_credentials.go 46 | func hostIsAzureContainerRegistry(host string) bool { 47 | for _, v := range []string{".azurecr.io", ".azurecr.cn", ".azurecr.de", ".azurecr.us"} { 48 | if strings.HasSuffix(host, v) { 49 | return true 50 | } 51 | } 52 | return false 53 | } 54 | -------------------------------------------------------------------------------- /pkg/registry/azure_test.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test_HostIsAzureContainerRegistry(t *testing.T) { 8 | for _, v := range []struct { 9 | host string 10 | isACR bool 11 | }{ 12 | { 13 | host: "azurecr.io", 14 | isACR: false, 15 | }, 16 | { 17 | host: "", 18 | isACR: false, 19 | }, 20 | { 21 | host: "gcr.io", 22 | isACR: false, 23 | }, 24 | { 25 | host: "notazurecr.io", 26 | isACR: false, 27 | }, 28 | { 29 | host: "example.azurecr.io.not", 30 | isACR: false, 31 | }, 32 | // Public cloud 33 | { 34 | host: "example.azurecr.io", 35 | isACR: true, 36 | }, 37 | // Sovereign clouds 38 | { 39 | host: "example.azurecr.cn", 40 | isACR: true, 41 | }, 42 | { 43 | host: "example.azurecr.de", 44 | isACR: true, 45 | }, 46 | { 47 | host: "example.azurecr.us", 48 | isACR: true, 49 | }, 50 | } { 51 | result := hostIsAzureContainerRegistry(v.host) 52 | if result != v.isACR { 53 | t.Fatalf("For test %q, expected isACR = %v but got %v", v.host, v.isACR, result) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /pkg/registry/cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | 7 | "github.com/fluxcd/flux/pkg/image" 8 | ) 9 | 10 | type Reader interface { 11 | // GetKey gets the value at a key, along with its refresh deadline 12 | GetKey(k Keyer) ([]byte, time.Time, error) 13 | } 14 | 15 | type Writer interface { 16 | // SetKey sets the value at a key, along with its refresh deadline 17 | SetKey(k Keyer, deadline time.Time, v []byte) error 18 | } 19 | 20 | type Client interface { 21 | Reader 22 | Writer 23 | } 24 | 25 | // An interface to provide the key under which to store the data 26 | // Use the full path to image for the memcache key because there 27 | // might be duplicates from other registries 28 | type Keyer interface { 29 | Key() string 30 | } 31 | 32 | type manifestKey struct { 33 | fullRepositoryPath, reference string 34 | } 35 | 36 | func NewManifestKey(image image.CanonicalRef) Keyer { 37 | return &manifestKey{image.CanonicalName().String(), image.Tag} 38 | } 39 | 40 | func (k *manifestKey) Key() string { 41 | return strings.Join([]string{ 42 | "registryhistoryv3", // Bump the version number if the cache format changes 43 | k.fullRepositoryPath, 44 | k.reference, 45 | }, "|") 46 | } 47 | 48 | type repoKey struct { 49 | fullRepositoryPath string 50 | } 51 | 52 | func NewRepositoryKey(repo image.CanonicalName) Keyer { 53 | return &repoKey{repo.String()} 54 | } 55 | 56 | func (k *repoKey) Key() string { 57 | return strings.Join([]string{ 58 | "registryrepov4", // Bump the version number if the cache format changes 59 | k.fullRepositoryPath, 60 | }, "|") 61 | } 62 | -------------------------------------------------------------------------------- /pkg/registry/cache/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | This package implements an image metadata cache given a backing k-v 3 | store. 4 | 5 | The interface `Client` stands in for the k-v store (e.g., memcached, 6 | in the subpackage); `Cache` implements registry.Registry given a 7 | `Client`. 8 | 9 | The `Warmer` is for continually refreshing the cache by fetching new 10 | metadata from the original image registries. 11 | */ 12 | package cache 13 | -------------------------------------------------------------------------------- /pkg/registry/cache/memcached/memcached_test.go: -------------------------------------------------------------------------------- 1 | // +build integration 2 | 3 | package memcached 4 | 5 | import ( 6 | "flag" 7 | "github.com/go-kit/kit/log" 8 | "os" 9 | "strings" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | var ( 15 | memcachedIPs = flag.String("memcached-ips", "127.0.0.1:11211", "space-separated host:port values for memcached to connect to") 16 | ) 17 | 18 | var val = []byte("test bytes") 19 | 20 | var key = testKey("test") 21 | 22 | type testKey string 23 | 24 | func (t testKey) Key() string { 25 | return string(t) 26 | } 27 | 28 | func TestMemcache_ExpiryReadWrite(t *testing.T) { 29 | // Memcache client 30 | mc := NewFixedServerMemcacheClient(MemcacheConfig{ 31 | Timeout: time.Second, 32 | UpdateInterval: 1 * time.Minute, 33 | Logger: log.With(log.NewLogfmtLogger(os.Stderr), "component", "memcached"), 34 | }, strings.Fields(*memcachedIPs)...) 35 | 36 | // Set some dummy data 37 | now := time.Now().Round(time.Second) 38 | err := mc.SetKey(key, now, val) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | 43 | cached, deadline, err := mc.GetKey(key) 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | if !deadline.Equal(now) { 48 | t.Fatalf("Deadline should be %s, but is %s", now.String(), deadline.String()) 49 | } 50 | 51 | if string(cached) != string(val) { 52 | t.Fatalf("Should have returned %q, but got %q", string(val), string(cached)) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /pkg/registry/cache/monitoring.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/go-kit/kit/metrics/prometheus" 8 | stdprometheus "github.com/prometheus/client_golang/prometheus" 9 | 10 | fluxmetrics "github.com/fluxcd/flux/pkg/metrics" 11 | ) 12 | 13 | var ( 14 | cacheRequestDuration = prometheus.NewHistogramFrom(stdprometheus.HistogramOpts{ 15 | Namespace: "flux", 16 | Subsystem: "cache", 17 | Name: "request_duration_seconds", 18 | Help: "Duration of cache requests, in seconds.", 19 | Buckets: stdprometheus.DefBuckets, 20 | }, []string{fluxmetrics.LabelMethod, fluxmetrics.LabelSuccess}) 21 | ) 22 | 23 | type instrumentedClient struct { 24 | next Client 25 | } 26 | 27 | func InstrumentClient(c Client) Client { 28 | return &instrumentedClient{ 29 | next: c, 30 | } 31 | } 32 | 33 | func (i *instrumentedClient) GetKey(k Keyer) (_ []byte, ex time.Time, err error) { 34 | defer func(begin time.Time) { 35 | cacheRequestDuration.With( 36 | fluxmetrics.LabelMethod, "GetKey", 37 | fluxmetrics.LabelSuccess, fmt.Sprint(err == nil), 38 | ).Observe(time.Since(begin).Seconds()) 39 | }(time.Now()) 40 | return i.next.GetKey(k) 41 | } 42 | 43 | func (i *instrumentedClient) SetKey(k Keyer, d time.Time, v []byte) (err error) { 44 | defer func(begin time.Time) { 45 | cacheRequestDuration.With( 46 | fluxmetrics.LabelMethod, "SetKey", 47 | fluxmetrics.LabelSuccess, fmt.Sprint(err == nil), 48 | ).Observe(time.Since(begin).Seconds()) 49 | }(time.Now()) 50 | return i.next.SetKey(k, d, v) 51 | } 52 | -------------------------------------------------------------------------------- /pkg/registry/cache/registry_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | "time" 7 | 8 | "github.com/fluxcd/flux/pkg/image" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | // mockStorage holds a fixed ImageRepository item. 13 | type mockStorage struct { 14 | Item ImageRepository 15 | } 16 | 17 | // GetKey will always return the same item from the storage, 18 | // and does not care about the key it receives. 19 | func (m *mockStorage) GetKey(k Keyer) ([]byte, time.Time, error) { 20 | b, err := json.Marshal(m.Item) 21 | if err != nil { 22 | return []byte{}, time.Time{}, err 23 | } 24 | return b, time.Time{}, nil 25 | } 26 | 27 | // appendImage adds an image to the mocked storage item. 28 | func (m *mockStorage) appendImage(i image.Info) { 29 | tag := i.ID.Tag 30 | 31 | m.Item.Images[tag] = i 32 | m.Item.Tags = append(m.Item.Tags, tag) 33 | } 34 | 35 | func mockReader() *mockStorage { 36 | return &mockStorage{ 37 | Item: ImageRepository{ 38 | RepositoryMetadata: image.RepositoryMetadata{ 39 | Tags: []string{}, 40 | Images: map[string]image.Info{}, 41 | }, 42 | LastUpdate: time.Now(), 43 | }, 44 | } 45 | } 46 | 47 | func Test_WhitelabelDecorator(t *testing.T) { 48 | r := mockReader() 49 | 50 | // Image with no timestamp label 51 | r.appendImage(mustMakeInfo("docker.io/fluxcd/flux:equal", time.Time{}, time.Now().UTC())) 52 | // Image with a timestamp label 53 | r.appendImage(mustMakeInfo("docker.io/fluxcd/flux:label", time.Now().Add(-10*time.Second).UTC(), time.Now().UTC())) 54 | 55 | c := Cache{r, []Decorator{TimestampLabelWhitelist{"index.docker.io/fluxcd/*"}}} 56 | 57 | rm, err := c.GetImageRepositoryMetadata(image.Name{}) 58 | assert.NoError(t, err) 59 | 60 | assert.Equal(t, r.Item.Images["equal"].CreatedAt, rm.Images["equal"].CreatedAt) 61 | assert.Equal(t, r.Item.Images["label"].Labels.Created, rm.Images["label"].CreatedAt) 62 | } 63 | 64 | func mustMakeInfo(ref string, label time.Time, created time.Time) image.Info { 65 | r, err := image.ParseRef(ref) 66 | if err != nil { 67 | panic(err) 68 | } 69 | var labels image.Labels 70 | if !label.IsZero() { 71 | labels.Created = label 72 | } 73 | return image.Info{ID: r, Labels: labels, CreatedAt: created} 74 | } 75 | -------------------------------------------------------------------------------- /pkg/registry/cache/repocachemanager_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httptest" 7 | "net/url" 8 | "os" 9 | "testing" 10 | "time" 11 | 12 | "github.com/go-kit/kit/log" 13 | "github.com/stretchr/testify/assert" 14 | 15 | "github.com/fluxcd/flux/pkg/image" 16 | "github.com/fluxcd/flux/pkg/registry" 17 | ) 18 | 19 | func Test_ClientTimeouts(t *testing.T) { 20 | timeout := 1 * time.Millisecond 21 | server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { 22 | // make sure we exceed the timeout 23 | time.Sleep(timeout * 10) 24 | })) 25 | defer server.Close() 26 | url, err := url.Parse(server.URL) 27 | assert.NoError(t, err) 28 | logger := log.NewLogfmtLogger(os.Stdout) 29 | cf := ®istry.RemoteClientFactory{ 30 | Logger: log.NewLogfmtLogger(os.Stdout), 31 | Limiters: nil, 32 | Trace: false, 33 | InsecureHosts: []string{url.Host}, 34 | } 35 | name := image.Name{ 36 | Domain: url.Host, 37 | Image: "foo/bar", 38 | } 39 | rcm, err := newRepoCacheManager( 40 | time.Now(), 41 | name, 42 | cf, 43 | registry.NoCredentials(), 44 | timeout, 45 | 1, 46 | false, 47 | logger, 48 | nil, 49 | ) 50 | assert.NoError(t, err) 51 | _, err = rcm.getTags(context.Background()) 52 | assert.Error(t, err) 53 | assert.Equal(t, "client timeout (1ms) exceeded", err.Error()) 54 | } 55 | -------------------------------------------------------------------------------- /pkg/registry/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | This package has types for dealing with image registries (e.g., 3 | quay.io, DockerHub, Google Container Registry, ..). 4 | */ 5 | package registry 6 | -------------------------------------------------------------------------------- /pkg/registry/gcp.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | const ( 10 | gcpDefaultTokenURL = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token" 11 | ) 12 | 13 | type gceToken struct { 14 | AccessToken string `json:"access_token"` 15 | ExpiresIn int `json:"expires_in"` 16 | TokenType string `json:"token_type"` 17 | } 18 | 19 | func GetGCPOauthToken(host string) (creds, error) { 20 | request, err := http.NewRequest("GET", gcpDefaultTokenURL, nil) 21 | if err != nil { 22 | return creds{}, err 23 | } 24 | 25 | request.Header.Add("Metadata-Flavor", "Google") 26 | 27 | client := &http.Client{} 28 | response, err := client.Do(request) 29 | if err != nil { 30 | return creds{}, err 31 | } 32 | 33 | if response.StatusCode != http.StatusOK { 34 | return creds{}, fmt.Errorf("unexpected status from metadata service: %s", response.Status) 35 | } 36 | 37 | var token gceToken 38 | decoder := json.NewDecoder(response.Body) 39 | if err := decoder.Decode(&token); err != nil { 40 | return creds{}, err 41 | } 42 | 43 | if err := response.Body.Close(); err != nil { 44 | return creds{}, err 45 | } 46 | 47 | return creds{ 48 | registry: host, 49 | provenance: "", 50 | username: "oauth2accesstoken", 51 | password: token.AccessToken}, nil 52 | } 53 | -------------------------------------------------------------------------------- /pkg/registry/imageentry_test.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | "github.com/fluxcd/flux/pkg/image" 11 | ) 12 | 13 | // Check that the ImageEntry type can be round-tripped via JSON. 14 | func TestImageEntryRoundtrip(t *testing.T) { 15 | 16 | test := func(t *testing.T, entry ImageEntry) { 17 | bytes, err := json.Marshal(entry) 18 | assert.NoError(t, err) 19 | 20 | var entry2 ImageEntry 21 | assert.NoError(t, json.Unmarshal(bytes, &entry2)) 22 | assert.Equal(t, entry, entry2) 23 | } 24 | 25 | ref, err := image.ParseRef("docker.io/fluxcd/flux:1.0.0") 26 | assert.NoError(t, err) 27 | 28 | info := image.Info{ 29 | ID: ref, 30 | CreatedAt: time.Now().UTC(), // to UTC since we unmarshal times in UTC 31 | } 32 | 33 | entry := ImageEntry{ 34 | Info: info, 35 | } 36 | t.Run("With an info", func(t *testing.T) { test(t, entry) }) 37 | t.Run("With an excluded reason", func(t *testing.T) { 38 | entry.Info = image.Info{} 39 | entry.ExcludedReason = "just because" 40 | test(t, entry) 41 | }) 42 | } 43 | 44 | // Check that existing entries, which are image.Info, will parse into 45 | // the ImageEntry struct. 46 | func TestImageInfoParsesAsEntry(t *testing.T) { 47 | ref, err := image.ParseRef("docker.io/fluxcd/flux:1.0.0") 48 | assert.NoError(t, err) 49 | info := image.Info{ 50 | ID: ref, 51 | CreatedAt: time.Now().UTC(), // to UTC since we unmarshal times in UTC 52 | } 53 | 54 | bytes, err := json.Marshal(info) 55 | assert.NoError(t, err) 56 | 57 | var entry2 ImageEntry 58 | assert.NoError(t, json.Unmarshal(bytes, &entry2)) 59 | assert.Equal(t, info, entry2.Info) 60 | } 61 | -------------------------------------------------------------------------------- /pkg/registry/mock/mock.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/pkg/errors" 7 | 8 | "github.com/fluxcd/flux/pkg/image" 9 | "github.com/fluxcd/flux/pkg/registry" 10 | ) 11 | 12 | type Client struct { 13 | ManifestFn func(ref string) (registry.ImageEntry, error) 14 | TagsFn func() ([]string, error) 15 | } 16 | 17 | func (m *Client) Manifest(ctx context.Context, tag string) (registry.ImageEntry, error) { 18 | return m.ManifestFn(tag) 19 | } 20 | 21 | func (m *Client) Tags(context.Context) ([]string, error) { 22 | return m.TagsFn() 23 | } 24 | 25 | var _ registry.Client = &Client{} 26 | 27 | type ClientFactory struct { 28 | Client registry.Client 29 | Err error 30 | } 31 | 32 | func (m *ClientFactory) ClientFor(repository image.CanonicalName, creds registry.Credentials) (registry.Client, error) { 33 | return m.Client, m.Err 34 | } 35 | 36 | func (_ *ClientFactory) Succeed(_ image.CanonicalName) { 37 | return 38 | } 39 | 40 | var _ registry.ClientFactory = &ClientFactory{} 41 | 42 | type Registry struct { 43 | Images []image.Info 44 | Err error 45 | } 46 | 47 | func (m *Registry) GetImageRepositoryMetadata(id image.Name) (image.RepositoryMetadata, error) { 48 | result := image.RepositoryMetadata{ 49 | Images: map[string]image.Info{}, 50 | } 51 | for _, i := range m.Images { 52 | // include only if it's the same repository in the same place 53 | if i.ID.Image == id.Image { 54 | tag := i.ID.Tag 55 | result.Tags = append(result.Tags, tag) 56 | result.Images[tag] = i 57 | } 58 | } 59 | return result, m.Err 60 | } 61 | 62 | func (m *Registry) GetImage(id image.Ref) (image.Info, error) { 63 | for _, i := range m.Images { 64 | if i.ID.String() == id.String() { 65 | return i, nil 66 | } 67 | } 68 | return image.Info{}, errors.New("not found") 69 | } 70 | 71 | var _ registry.Registry = &Registry{} 72 | -------------------------------------------------------------------------------- /pkg/registry/registry.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/fluxcd/flux/pkg/image" 7 | ) 8 | 9 | var ( 10 | ErrNoImageData = errors.New("image data not available") 11 | ErrImageScanDisabled = errors.New("cannot perfom operation, image scanning is disabled") 12 | ) 13 | 14 | // Registry is a store of image metadata. 15 | type Registry interface { 16 | GetImageRepositoryMetadata(image.Name) (image.RepositoryMetadata, error) 17 | GetImage(image.Ref) (image.Info, error) 18 | } 19 | 20 | // ImageCreds is a record of which images need which credentials, 21 | // which is supplied to us (probably by interrogating the cluster) 22 | type ImageCreds map[image.Name]Credentials 23 | 24 | // ImageScanDisabledRegistry is used when image scanning is disabled 25 | type ImageScanDisabledRegistry struct{} 26 | 27 | func (i ImageScanDisabledRegistry) GetImageRepositoryMetadata(image.Name) (image.RepositoryMetadata, error) { 28 | return image.RepositoryMetadata{}, ErrImageScanDisabled 29 | } 30 | 31 | func (i ImageScanDisabledRegistry) GetImage(image.Ref) (image.Info, error) { 32 | return image.Info{}, ErrImageScanDisabled 33 | } 34 | -------------------------------------------------------------------------------- /pkg/release/errors.go: -------------------------------------------------------------------------------- 1 | package release 2 | 3 | import ( 4 | fluxerr "github.com/fluxcd/flux/pkg/errors" 5 | ) 6 | 7 | func MakeReleaseError(err error) *fluxerr.Error { 8 | return &fluxerr.Error{ 9 | Type: fluxerr.User, 10 | Help: `The release process failed, with this message: 11 | 12 | ` + err.Error() + ` 13 | 14 | This may be because of a limitation in the formats of file Flux can 15 | deal with. See 16 | 17 | https://fluxcd.io/legacy/flux/requirements/ 18 | 19 | for those limitations. 20 | 21 | If your files appear to meet the requirements, it may simply be a bug 22 | in Flux. Please report it at 23 | 24 | https://github.com/fluxcd/flux/issues 25 | 26 | and try to include the problematic manifest, if it can be identified. 27 | `, 28 | Err: err, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pkg/remote/doc.go: -------------------------------------------------------------------------------- 1 | // Package remote has the types for the protocol between a daemon and 2 | // an upstream service. 3 | 4 | package remote 5 | 6 | // This whole package can be moved to weaveworks/flux-adapter, once we 7 | // no longer `--connect` from fluxd. (Or before that, if we import it 8 | // from there.) 9 | -------------------------------------------------------------------------------- /pkg/remote/mock_test.go: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/fluxcd/flux/pkg/api" 7 | ) 8 | 9 | // Just test that the mock does its job. 10 | func TestMock(t *testing.T) { 11 | ServerTestBattery(t, func(mock api.Server) api.Server { return mock }) 12 | } 13 | -------------------------------------------------------------------------------- /pkg/remote/rpc/clientV10.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net/rpc" 7 | 8 | "github.com/fluxcd/flux/pkg/api/v10" 9 | "github.com/fluxcd/flux/pkg/api/v6" 10 | "github.com/fluxcd/flux/pkg/remote" 11 | ) 12 | 13 | // RPCClientV10 is the rpc-backed implementation of a server, for 14 | // talking to remote daemons. This version introduces methods which accept an 15 | // options struct as the first argument. e.g. ListImagesWithOptions 16 | type RPCClientV10 struct { 17 | *RPCClientV9 18 | } 19 | 20 | type clientV10 interface { 21 | v10.Server 22 | v10.Upstream 23 | } 24 | 25 | var _ clientV10 = &RPCClientV10{} 26 | 27 | // NewClientV10 creates a new rpc-backed implementation of the server. 28 | func NewClientV10(conn io.ReadWriteCloser) *RPCClientV10 { 29 | return &RPCClientV10{NewClientV9(conn)} 30 | } 31 | 32 | func (p *RPCClientV10) ListImagesWithOptions(ctx context.Context, opts v10.ListImagesOptions) ([]v6.ImageStatus, error) { 33 | var resp ListImagesResponse 34 | if err := requireServiceSpecKinds(opts.Spec, supportedKindsV8); err != nil { 35 | return resp.Result, remote.UnsupportedResourceKind(err) 36 | } 37 | 38 | err := p.client.Call("RPCServer.ListImagesWithOptions", opts, &resp) 39 | if err != nil { 40 | if _, ok := err.(rpc.ServerError); !ok && err != nil { 41 | err = remote.FatalError{err} 42 | } 43 | } else if resp.ApplicationError != nil { 44 | err = resp.ApplicationError 45 | } 46 | return resp.Result, err 47 | } 48 | -------------------------------------------------------------------------------- /pkg/remote/rpc/clientV11.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net/rpc" 7 | 8 | "github.com/fluxcd/flux/pkg/api/v11" 9 | "github.com/fluxcd/flux/pkg/api/v6" 10 | "github.com/fluxcd/flux/pkg/remote" 11 | ) 12 | 13 | // RPCClientV11 is the rpc-backed implementation of a server, for 14 | // talking to remote daemons. This version introduces methods which accept an 15 | // options struct as the first argument. e.g. ListServicesWithOptions 16 | type RPCClientV11 struct { 17 | *RPCClientV10 18 | } 19 | 20 | type clientV11 interface { 21 | v11.Server 22 | } 23 | 24 | var _ clientV11 = &RPCClientV11{} 25 | 26 | // NewClientV11 creates a new rpc-backed implementation of the server. 27 | func NewClientV11(conn io.ReadWriteCloser) *RPCClientV11 { 28 | return &RPCClientV11{NewClientV10(conn)} 29 | } 30 | 31 | func (p *RPCClientV11) ListServicesWithOptions(ctx context.Context, opts v11.ListServicesOptions) ([]v6.ControllerStatus, error) { 32 | var resp ListServicesResponse 33 | for _, svc := range opts.Services { 34 | if err := requireServiceIDKinds(svc, supportedKindsV8); err != nil { 35 | return resp.Result, remote.UnsupportedResourceKind(err) 36 | } 37 | } 38 | 39 | err := p.client.Call("RPCServer.ListServicesWithOptions", opts, &resp) 40 | listServicesRolloutStatus(resp.Result) 41 | if err != nil { 42 | if _, ok := err.(rpc.ServerError); !ok && err != nil { 43 | err = remote.FatalError{err} 44 | } 45 | } else if resp.ApplicationError != nil { 46 | err = resp.ApplicationError 47 | } 48 | return resp.Result, err 49 | } 50 | -------------------------------------------------------------------------------- /pkg/remote/rpc/clientV9.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net/rpc" 7 | 8 | "github.com/fluxcd/flux/pkg/api/v9" 9 | "github.com/fluxcd/flux/pkg/remote" 10 | ) 11 | 12 | type RPCClientV9 struct { 13 | *RPCClientV8 14 | } 15 | 16 | type clientV9 interface { 17 | v9.Server 18 | v9.Upstream 19 | } 20 | 21 | var _ clientV9 = &RPCClientV9{} 22 | 23 | func NewClientV9(conn io.ReadWriteCloser) *RPCClientV9 { 24 | return &RPCClientV9{NewClientV8(conn)} 25 | } 26 | 27 | func (p *RPCClientV9) NotifyChange(ctx context.Context, c v9.Change) error { 28 | var resp NotifyChangeResponse 29 | err := p.client.Call("RPCServer.NotifyChange", c, &resp) 30 | if err != nil { 31 | if _, ok := err.(rpc.ServerError); !ok && err != nil { 32 | err = remote.FatalError{err} 33 | } 34 | } else if resp.ApplicationError != nil { 35 | err = resp.ApplicationError 36 | } 37 | return err 38 | } 39 | -------------------------------------------------------------------------------- /pkg/remote/rpc/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This is a `net/rpc`-compatible implementation of a client and server 4 | for `flux/api.Server`. 5 | 6 | The purpose is to be able to access a daemon from an upstream 7 | service. The daemon makes an outbound connection (over, say, 8 | websockets), then the service can make RPC calls over that connection. 9 | 10 | On errors: 11 | 12 | Errors from the daemon can come in two varieties: application errors 13 | (i.e., a `*(flux/errors).Error`), and internal errors (any other 14 | `error`). We need to transmit these faithfully over `net/rpc`, which 15 | only accounts for `error` (and flattens them to strings for 16 | transmission). 17 | 18 | To send application errors, we construct response values that are 19 | effectively a union of the actual response type, and the error type. 20 | 21 | At the client end, we also need to deal with transmission errors -- 22 | e.g., a response timing out, or the connection closing abruptly. These 23 | are treated as "Fatal" errors; that is, they should result in a 24 | disconnection of the daemon as well as being returned to the caller. 25 | 26 | On versions: 27 | 28 | The RPC protocol is versioned, because server code (in the daemon) is 29 | deployed independently of client code (in the upstream service). 30 | 31 | We share the RPC protocol versions with the API, because the endpoint 32 | for connecting to an upstream service (`/api/flux//register`) 33 | is part of the API. 34 | 35 | Since one client (upstream service) has connections to many servers 36 | (daemons), it's the client that has explicit versions in the code. The 37 | server code always implements just the most recent version. 38 | 39 | For backwards-incompatible changes, we must bump the protocol version 40 | (and create a new `RegisterDaemon` endpoint). 41 | 42 | On contexts: 43 | 44 | Sadly, `net/rpc` does not support context.Context, and never will. So 45 | we must ignore the contexts passed in. If we change the RPC mechanism, 46 | we may be able to address this. 47 | 48 | */ 49 | package rpc 50 | -------------------------------------------------------------------------------- /pkg/remote/rpc/rpc_test.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "reflect" 8 | "testing" 9 | "time" 10 | 11 | "github.com/fluxcd/flux/pkg/api" 12 | "github.com/fluxcd/flux/pkg/remote" 13 | ) 14 | 15 | func pipes() (io.ReadWriteCloser, io.ReadWriteCloser) { 16 | type end struct { 17 | io.Reader 18 | io.WriteCloser 19 | } 20 | 21 | serverReader, clientWriter := io.Pipe() 22 | clientReader, serverWriter := io.Pipe() 23 | return end{clientReader, clientWriter}, end{serverReader, serverWriter} 24 | } 25 | 26 | func TestRPC(t *testing.T) { 27 | wrap := func(mock api.Server) api.Server { 28 | clientConn, serverConn := pipes() 29 | 30 | server, err := NewServer(mock, 10*time.Second) 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | go server.ServeConn(serverConn) 35 | return NewClientV11(clientConn) 36 | } 37 | remote.ServerTestBattery(t, wrap) 38 | } 39 | 40 | // --- 41 | 42 | type poorReader struct{} 43 | 44 | func (r poorReader) Read(p []byte) (int, error) { 45 | return 0, errors.New("failure to read") 46 | } 47 | 48 | // Return a pair of connections made of pipes, in which the first 49 | // connection will fail Reads. 50 | func faultyPipes() (io.ReadWriteCloser, io.ReadWriteCloser) { 51 | type end struct { 52 | io.Reader 53 | io.WriteCloser 54 | } 55 | 56 | serverReader, clientWriter := io.Pipe() 57 | _, serverWriter := io.Pipe() 58 | return end{poorReader{}, clientWriter}, end{serverReader, serverWriter} 59 | } 60 | 61 | func TestBadRPC(t *testing.T) { 62 | ctx := context.Background() 63 | mock := &remote.MockServer{} 64 | clientConn, serverConn := faultyPipes() 65 | server, err := NewServer(mock, 10*time.Second) 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | go server.ServeConn(serverConn) 70 | 71 | client := NewClientV9(clientConn) 72 | if err = client.Ping(ctx); err == nil { 73 | t.Error("expected error from RPC system, got nil") 74 | } 75 | if _, ok := err.(remote.FatalError); !ok { 76 | t.Errorf("expected remote.FatalError from RPC mechanism, got %s", reflect.TypeOf(err)) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /pkg/resource/id_test.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestResourceIDParsing(t *testing.T) { 8 | type test struct { 9 | name, id string 10 | } 11 | valid := []test{ 12 | {"full", "namespace:kind/name"}, 13 | {"legacy", "namespace/service"}, 14 | {"dots", "namespace:kind/name.with.dots"}, 15 | {"colons", "namespace:kind/name:with:colons"}, 16 | {"punctuation in general", "name-space:ki_nd/punc_tu:a.tion-rules"}, 17 | {"cluster-scope resource", ":namespace/foo"}, 18 | } 19 | invalid := []test{ 20 | {"unqualified", "justname"}, 21 | {"dots in namespace", "name.space:kind/name"}, 22 | {"too many colons", "namespace:kind:name"}, 23 | } 24 | 25 | for _, tc := range valid { 26 | t.Run(tc.name, func(t *testing.T) { 27 | if _, err := ParseID(tc.id); err != nil { 28 | t.Error(err) 29 | } 30 | }) 31 | } 32 | for _, tc := range invalid { 33 | t.Run(tc.name, func(t *testing.T) { 34 | if _, err := ParseID(tc.id); err == nil { 35 | t.Errorf("expected %q to be considered invalid", tc.id) 36 | } 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /pkg/resource/policy.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/fluxcd/flux/pkg/policy" 7 | ) 8 | 9 | // ChangeForPolicyUpdate evaluates a policy update with respect to a 10 | // workload. The reason this exists at all is that an `Update` can 11 | // include qualified policies, for example "tag all containers"; and 12 | // to make actual changes, we need to examine the workload to which 13 | // it's to be applied. 14 | // 15 | // This also translates policy deletion to empty values (i.e., `""`), 16 | // to make it easy to use as command-line arguments or environment 17 | // variables. When represented in manifests, policies are expected to 18 | // have a non-empty value when present, even if it's `"true"`; so an 19 | // empty value can safely denote deletion. 20 | func ChangesForPolicyUpdate(workload Workload, update PolicyUpdate) (map[string]string, error) { 21 | add, del := update.Add, update.Remove 22 | // We may be sent the pseudo-policy `policy.TagAll`, which means 23 | // apply this filter to all containers. To do so, we need to know 24 | // what all the containers are. 25 | if tagAll, ok := update.Add.Get(policy.TagAll); ok { 26 | add = add.Without(policy.TagAll) 27 | for _, container := range workload.Containers() { 28 | if tagAll == policy.PatternAll.String() { 29 | del = del.Add(policy.TagPrefix(container.Name)) 30 | } else { 31 | add = add.Set(policy.TagPrefix(container.Name), tagAll) 32 | } 33 | } 34 | } 35 | 36 | result := map[string]string{} 37 | for pol, val := range add { 38 | if policy.Tag(pol) && !policy.NewPattern(val).Valid() { 39 | return nil, fmt.Errorf("invalid tag pattern: %q", val) 40 | } 41 | result[string(pol)] = val 42 | } 43 | for pol, _ := range del { 44 | result[string(pol)] = "" 45 | } 46 | return result, nil 47 | } 48 | 49 | type PolicyUpdates map[ID]PolicyUpdate 50 | 51 | type PolicyUpdate struct { 52 | Add policy.Set `json:"add"` 53 | Remove policy.Set `json:"remove"` 54 | } 55 | -------------------------------------------------------------------------------- /pkg/resource/resource.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/fluxcd/flux/pkg/image" 5 | "github.com/fluxcd/flux/pkg/policy" 6 | ) 7 | 8 | // For the minute we just care about 9 | type Resource interface { 10 | ResourceID() ID // name, to correlate with what's in the cluster 11 | Policies() policy.Set // policy for this resource; e.g., whether it is locked, automated, ignored 12 | Source() string // where did this come from (informational) 13 | Bytes() []byte // the definition, for sending to cluster.Sync 14 | } 15 | 16 | type Container struct { 17 | Name string 18 | Image image.Ref 19 | } 20 | 21 | type Workload interface { 22 | Resource 23 | Containers() []Container 24 | // SetContainerImage mutates this workload so that the container 25 | // named has the image given. This is not expected to have an 26 | // effect on any underlying file or cluster resource. 27 | SetContainerImage(container string, ref image.Ref) error 28 | } 29 | -------------------------------------------------------------------------------- /pkg/ssh/keyring.go: -------------------------------------------------------------------------------- 1 | package ssh 2 | 3 | // KeyRing is an abstraction providing access to a managed SSH key pair. Whilst 4 | // the public half is available in byte form, the private half is left on the 5 | // filesystem to avoid memory management issues. 6 | type KeyRing interface { 7 | KeyPair() (publicKey PublicKey, privateKeyPath string) 8 | Regenerate() error 9 | } 10 | 11 | type sshKeyRing struct{} 12 | 13 | // NewNopSSHKeyRing returns a KeyRing that doesn't do anything. 14 | // It is meant for local development purposes when running fluxd outside a Kubernetes container. 15 | func NewNopSSHKeyRing() KeyRing { 16 | return &sshKeyRing{} 17 | } 18 | 19 | func (skr *sshKeyRing) KeyPair() (PublicKey, string) { 20 | return PublicKey{}, "" 21 | } 22 | 23 | func (skr *sshKeyRing) Regenerate() error { 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /pkg/sync/mock.go: -------------------------------------------------------------------------------- 1 | package sync 2 | 3 | import ( 4 | "github.com/fluxcd/flux/pkg/policy" 5 | "github.com/fluxcd/flux/pkg/resource" 6 | ) 7 | 8 | type rsc struct { 9 | bytes []byte 10 | Kind string 11 | Meta struct { 12 | Namespace string 13 | Name string 14 | } 15 | } 16 | 17 | type rscIgnorePolicy struct { 18 | rsc 19 | } 20 | 21 | func (rs rsc) Source() string { 22 | return "" 23 | } 24 | 25 | func (rs rsc) Bytes() []byte { 26 | return []byte{} 27 | } 28 | 29 | func (rs rsc) ResourceID() resource.ID { 30 | return resource.MakeID(rs.Meta.Namespace, rs.Kind, rs.Meta.Name) 31 | } 32 | 33 | func (rs rsc) Policy() policy.Set { 34 | p := policy.Set{} 35 | return p 36 | } 37 | 38 | func (ri rscIgnorePolicy) Policy() policy.Set { 39 | p := policy.Set{} 40 | p[policy.Ignore] = "true" 41 | return p 42 | } 43 | 44 | func mockResourceWithoutIgnorePolicy(kind, namespace, name string) rsc { 45 | r := rsc{Kind: kind} 46 | r.Meta.Namespace = namespace 47 | r.Meta.Name = name 48 | return r 49 | } 50 | 51 | func mockResourceWithIgnorePolicy(kind, namespace, name string) rscIgnorePolicy { 52 | ri := rscIgnorePolicy{rsc{Kind: kind}} 53 | ri.Meta.Namespace = namespace 54 | ri.Meta.Name = name 55 | return ri 56 | } 57 | -------------------------------------------------------------------------------- /pkg/sync/provider.go: -------------------------------------------------------------------------------- 1 | package sync 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | const ( 9 | // GitTagStateMode is a mode of state management where Flux uses a git tag for managing Flux state 10 | GitTagStateMode = "git" 11 | 12 | // NativeStateMode is a mode of state management where Flux uses native Kubernetes resources for managing Flux state 13 | NativeStateMode = "secret" 14 | ) 15 | 16 | // VerifySignaturesMode represents the strategy to use when choosing which commits to GPG-verify between the flux sync tag and the tip of the flux branch 17 | type VerifySignaturesMode string 18 | 19 | const ( 20 | // VerifySignaturesModeDefault - get the default behavior when casting 21 | VerifySignaturesModeDefault = "" 22 | 23 | // VerifySignaturesModeNone (default) - don't verify any commits 24 | VerifySignaturesModeNone = "none" 25 | 26 | // VerifySignaturesModeAll - consider all possible commits 27 | VerifySignaturesModeAll = "all" 28 | 29 | // VerifySignaturesModeFirstParent - consider only commits on the chain of 30 | // first parents (i.e. don't consider commits merged from another branch) 31 | VerifySignaturesModeFirstParent = "first-parent" 32 | ) 33 | 34 | // ToVerifySignaturesMode converts a string to a VerifySignaturesMode 35 | func ToVerifySignaturesMode(s string) (VerifySignaturesMode, error) { 36 | switch s { 37 | case VerifySignaturesModeDefault: 38 | return VerifySignaturesModeNone, nil 39 | case VerifySignaturesModeNone: 40 | return VerifySignaturesModeNone, nil 41 | case VerifySignaturesModeAll: 42 | return VerifySignaturesModeAll, nil 43 | case VerifySignaturesModeFirstParent: 44 | return VerifySignaturesModeFirstParent, nil 45 | default: 46 | return VerifySignaturesModeNone, fmt.Errorf("'%s' is not a valid git-verify-signatures-mode", s) 47 | } 48 | } 49 | 50 | type State interface { 51 | // GetRevision fetches the recorded revision, returning an empty 52 | // string if none has been recorded yet. 53 | GetRevision(ctx context.Context) (string, error) 54 | // UpdateMarker records the high water mark 55 | UpdateMarker(ctx context.Context, revision string) error 56 | // DeleteMarker removes the high water mark 57 | DeleteMarker(ctx context.Context) error 58 | // String returns a string representation of where the state is 59 | // recorded (e.g., for referring to it in logs) 60 | String() string 61 | } 62 | -------------------------------------------------------------------------------- /pkg/sync/sync.go: -------------------------------------------------------------------------------- 1 | package sync 2 | 3 | import ( 4 | "github.com/fluxcd/flux/pkg/cluster" 5 | "github.com/fluxcd/flux/pkg/resource" 6 | ) 7 | 8 | // Syncer has the methods we need to be able to compile and run a sync 9 | type Syncer interface { 10 | Sync(cluster.SyncSet) error 11 | } 12 | 13 | // Sync synchronises the cluster to the files under a directory. 14 | func Sync(setName string, repoResources map[string]resource.Resource, clus Syncer) error { 15 | set := makeSet(setName, repoResources) 16 | if err := clus.Sync(set); err != nil { 17 | return err 18 | } 19 | return nil 20 | } 21 | 22 | func makeSet(name string, repoResources map[string]resource.Resource) cluster.SyncSet { 23 | s := cluster.SyncSet{Name: name} 24 | var resources []resource.Resource 25 | for _, res := range repoResources { 26 | resources = append(resources, res) 27 | } 28 | s.Resources = resources 29 | return s 30 | } 31 | -------------------------------------------------------------------------------- /pkg/update/automated_test.go: -------------------------------------------------------------------------------- 1 | package update 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/fluxcd/flux/pkg/resource" 7 | ) 8 | 9 | func TestCommitMessage(t *testing.T) { 10 | automated := Automated{} 11 | result := Result{ 12 | resource.MakeID("ns", "kind", "1"): { 13 | Status: ReleaseStatusSuccess, 14 | PerContainer: []ContainerUpdate{ 15 | {Target: mustParseRef("docker.io/image:v1")}, 16 | {Target: mustParseRef("docker.io/image:v2")}, 17 | {Target: mustParseRef("docker.io/image:v3")}, 18 | {Target: mustParseRef("docker.io/image:v4")}, 19 | {Target: mustParseRef("docker.io/image:v5")}, 20 | {Target: mustParseRef("docker.io/image:v6")}, 21 | {Target: mustParseRef("docker.io/image:v7")}, 22 | {Target: mustParseRef("docker.io/image:v8")}, 23 | {Target: mustParseRef("docker.io/image:v9")}, 24 | {Target: mustParseRef("docker.io/image:v10")}, 25 | {Target: mustParseRef("docker.io/image:v11")}, 26 | }, 27 | }, 28 | } 29 | result.ChangedImages() 30 | 31 | actual := automated.CommitMessage(result) 32 | expected := `Auto-release multiple (11) images 33 | 34 | - docker.io/image:v1 35 | - docker.io/image:v10 36 | - docker.io/image:v11 37 | - docker.io/image:v2 38 | - docker.io/image:v3 39 | - docker.io/image:v4 40 | - docker.io/image:v5 41 | - docker.io/image:v6 42 | - docker.io/image:v7 43 | - docker.io/image:v8 44 | - docker.io/image:v9 45 | ` 46 | if actual != expected { 47 | t.Fatalf("Expected git commit message: '%s', was '%s'", expected, actual) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /pkg/update/menu_unix.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package update 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/pkg/term" 9 | "golang.org/x/sys/unix" 10 | ) 11 | 12 | func terminalWidth() uint16 { 13 | ws, _ := unix.IoctlGetWinsize(int(os.Stdout.Fd()), unix.TIOCGWINSZ) 14 | if ws != nil && ws.Col != 0 { 15 | return ws.Col 16 | } 17 | return 9999 18 | } 19 | 20 | // See https://github.com/paulrademacher/climenu/blob/master/getchar.go 21 | func getChar() (ascii int, keyCode int, err error) { 22 | t, _ := term.Open("/dev/tty") 23 | term.RawMode(t) 24 | bs := make([]byte, 3) 25 | 26 | var numRead int 27 | numRead, err = t.Read(bs) 28 | if err != nil { 29 | return 30 | } 31 | if numRead == 3 && bs[0] == 27 && bs[1] == 91 { 32 | // Three-character control sequence, beginning with "ESC-[". 33 | 34 | // Since there are no ASCII codes for arrow keys, we use 35 | // Javascript key codes. 36 | if bs[2] == 65 { 37 | // Up 38 | keyCode = 38 39 | } else if bs[2] == 66 { 40 | // Down 41 | keyCode = 40 42 | } else if bs[2] == 67 { 43 | // Right 44 | keyCode = 39 45 | } else if bs[2] == 68 { 46 | // Left 47 | keyCode = 37 48 | } 49 | } else if numRead == 1 { 50 | ascii = int(bs[0]) 51 | } else { 52 | // Two characters read?? 53 | } 54 | t.Restore() 55 | t.Close() 56 | return 57 | } 58 | -------------------------------------------------------------------------------- /pkg/update/menu_win.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package update 4 | 5 | import "errors" 6 | 7 | func terminalWidth() uint16 { 8 | return 9999 9 | } 10 | 11 | func getChar() (ascii int, keyCode int, err error) { 12 | return 0, 0, errors.New("Error: Interactive mode is not supported on Windows") 13 | } 14 | -------------------------------------------------------------------------------- /pkg/update/metrics.go: -------------------------------------------------------------------------------- 1 | package update 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | fluxmetrics "github.com/fluxcd/flux/pkg/metrics" 8 | "github.com/go-kit/kit/metrics" 9 | "github.com/go-kit/kit/metrics/prometheus" 10 | stdprometheus "github.com/prometheus/client_golang/prometheus" 11 | ) 12 | 13 | var ( 14 | releaseDuration = prometheus.NewHistogramFrom(stdprometheus.HistogramOpts{ 15 | Namespace: "flux", 16 | Subsystem: "fluxsvc", 17 | Name: "release_duration_seconds", 18 | Help: "Release method duration in seconds.", 19 | Buckets: stdprometheus.DefBuckets, 20 | }, []string{fluxmetrics.LabelReleaseType, fluxmetrics.LabelReleaseKind, fluxmetrics.LabelSuccess}) 21 | stageDuration = prometheus.NewHistogramFrom(stdprometheus.HistogramOpts{ 22 | Namespace: "flux", 23 | Subsystem: "fluxsvc", 24 | Name: "release_stage_duration_seconds", 25 | Help: "Duration in seconds of each stage of a release, including dry-runs.", 26 | Buckets: stdprometheus.DefBuckets, 27 | }, []string{fluxmetrics.LabelStage}) 28 | ) 29 | 30 | func NewStageTimer(stage string) *metrics.Timer { 31 | return metrics.NewTimer(stageDuration.With(fluxmetrics.LabelStage, stage)) 32 | } 33 | 34 | func ObserveRelease(start time.Time, success bool, releaseType ReleaseType, releaseKind ReleaseKind) { 35 | releaseDuration.With( 36 | fluxmetrics.LabelSuccess, fmt.Sprint(success), 37 | fluxmetrics.LabelReleaseType, string(releaseType), 38 | fluxmetrics.LabelReleaseKind, string(releaseKind), 39 | ).Observe(time.Since(start).Seconds()) 40 | } 41 | -------------------------------------------------------------------------------- /pkg/update/print.go: -------------------------------------------------------------------------------- 1 | package update 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | // PrintResults outputs a result set to the `io.Writer` provided, at 8 | // the given level of verbosity: 9 | // - 2 = include skipped and ignored resources 10 | // - 1 = include skipped resources, exclude ignored resources 11 | // - 0 = exclude skipped and ignored resources 12 | func PrintResults(out io.Writer, results Result, verbosity int) { 13 | NewMenu(out, results, verbosity).Print() 14 | } 15 | -------------------------------------------------------------------------------- /pkg/update/spec.go: -------------------------------------------------------------------------------- 1 | package update 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | 7 | "github.com/fluxcd/flux/pkg/resource" 8 | ) 9 | 10 | const ( 11 | Images = "image" 12 | Policy = "policy" 13 | Auto = "auto" 14 | Sync = "sync" 15 | Containers = "containers" 16 | ) 17 | 18 | // How did this update get triggered? 19 | type Cause struct { 20 | Message string 21 | User string 22 | } 23 | 24 | // A tagged union for all (both) kinds of update. The type is just so 25 | // we know how to decode the rest of the struct. 26 | type Spec struct { 27 | Type string `json:"type"` 28 | Cause Cause `json:"cause"` 29 | Spec interface{} `json:"spec"` 30 | } 31 | 32 | func (spec *Spec) UnmarshalJSON(in []byte) error { 33 | var wire struct { 34 | Type string `json:"type"` 35 | Cause Cause `json:"cause"` 36 | SpecBytes json.RawMessage `json:"spec"` 37 | } 38 | 39 | if err := json.Unmarshal(in, &wire); err != nil { 40 | return err 41 | } 42 | spec.Type = wire.Type 43 | spec.Cause = wire.Cause 44 | switch wire.Type { 45 | case Policy: 46 | var update resource.PolicyUpdates 47 | if err := json.Unmarshal(wire.SpecBytes, &update); err != nil { 48 | return err 49 | } 50 | spec.Spec = update 51 | case Images: 52 | var update ReleaseImageSpec 53 | if err := json.Unmarshal(wire.SpecBytes, &update); err != nil { 54 | return err 55 | } 56 | spec.Spec = update 57 | case Auto: 58 | var update Automated 59 | if err := json.Unmarshal(wire.SpecBytes, &update); err != nil { 60 | return err 61 | } 62 | spec.Spec = update 63 | case Sync: 64 | var update ManualSync 65 | if err := json.Unmarshal(wire.SpecBytes, &update); err != nil { 66 | return err 67 | } 68 | spec.Spec = update 69 | case Containers: 70 | var update ReleaseContainersSpec 71 | if err := json.Unmarshal(wire.SpecBytes, &update); err != nil { 72 | return err 73 | } 74 | spec.Spec = update 75 | default: 76 | return errors.New("unknown spec type: " + wire.Type) 77 | } 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /pkg/update/spec_test.go: -------------------------------------------------------------------------------- 1 | package update 2 | 3 | import "testing" 4 | 5 | func TestParseImageSpec(t *testing.T) { 6 | parseSpec(t, "valid/image:tag", false) 7 | parseSpec(t, "image:tag", false) 8 | parseSpec(t, ":tag", true) 9 | parseSpec(t, "image:", true) 10 | parseSpec(t, "image", true) 11 | parseSpec(t, string(ImageSpecLatest), false) 12 | parseSpec(t, "", true) 13 | } 14 | 15 | func parseSpec(t *testing.T, image string, expectError bool) { 16 | spec, err := ParseImageSpec(image) 17 | isErr := (err != nil) 18 | if isErr != expectError { 19 | t.Fatalf("Expected error = %v for %q. Error = %q\n", expectError, image, err) 20 | } 21 | if !expectError && (string(spec) != image) { 22 | t.Fatalf("Expected string spec %q but got %q", image, string(spec)) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pkg/update/sync.go: -------------------------------------------------------------------------------- 1 | package update 2 | 3 | type ManualSync struct { 4 | } 5 | -------------------------------------------------------------------------------- /pkg/update/workload.go: -------------------------------------------------------------------------------- 1 | package update 2 | 3 | import ( 4 | "github.com/fluxcd/flux/pkg/cluster" 5 | "github.com/fluxcd/flux/pkg/resource" 6 | ) 7 | 8 | type WorkloadUpdate struct { 9 | ResourceID resource.ID 10 | Workload cluster.Workload 11 | Resource resource.Workload 12 | Updates []ContainerUpdate 13 | } 14 | 15 | type WorkloadFilter interface { 16 | Filter(WorkloadUpdate) WorkloadResult 17 | } 18 | 19 | func (s *WorkloadUpdate) Filter(filters ...WorkloadFilter) WorkloadResult { 20 | for _, f := range filters { 21 | fr := f.Filter(*s) 22 | if fr.Error != "" { 23 | return fr 24 | } 25 | } 26 | return WorkloadResult{} 27 | } 28 | -------------------------------------------------------------------------------- /snap/snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: fluxctl 2 | summary: fluxctl talks to Flux and helps you deploy your code 3 | description: | 4 | fluxctl talks to your Flux instance and exposes all its 5 | functionality to an easy to use command line interface. 6 | confinement: classic 7 | adopt-info: fluxctl 8 | base: core20 9 | 10 | parts: 11 | fluxctl: 12 | source: . 13 | override-pull: | 14 | snapcraftctl pull 15 | FLUX_TAG="$(curl -s 'https://api.github.com/repos/fluxcd/flux/releases/latest' | jq -r .tag_name)" 16 | set +e 17 | git describe --exact-match --tags $(git log -n1 --pretty='%h') --exclude 'chart-*' 18 | retVal=$? 19 | set -e 20 | if [ $retVal -eq 0 ]; then 21 | snapcraftctl set-version "$FLUX_TAG" 22 | snapcraftctl set-grade stable 23 | else 24 | GIT_REV="$(git rev-parse --short HEAD)" 25 | snapcraftctl set-version "$FLUX_TAG+$GIT_REV" 26 | snapcraftctl set-grade devel 27 | fi 28 | plugin: nil 29 | override-build: | 30 | export GOBIN=$SNAPCRAFT_PART_INSTALL/bin 31 | go build -o $GOBIN/fluxctl ./cmd/fluxctl 32 | build-environment: 33 | - GO111MODULE: 'on' 34 | - CGO_ENABLED: '0' 35 | build-packages: 36 | - curl 37 | - gcc 38 | - git 39 | - jq 40 | build-snaps: 41 | - go/1.17/stable 42 | stage: 43 | - -bin/fluxd 44 | - -bin/helm-operator 45 | 46 | apps: 47 | fluxctl: 48 | command: bin/fluxctl 49 | -------------------------------------------------------------------------------- /test/e2e/10_helm_chart.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | function setup() { 4 | load lib/env 5 | load lib/install 6 | load lib/poll 7 | 8 | kubectl create namespace "$FLUX_NAMESPACE" 9 | install_git_srv 10 | install_tiller 11 | install_flux_with_helm 12 | } 13 | 14 | @test "Helm chart installation smoke test" { 15 | # The gitconfig secret must exist and have the right value 16 | poll_until_equals "gitconfig secret" "${GITCONFIG}" "kubectl get secrets -n ${FLUX_NAMESPACE} gitconfig -ojsonpath={..data.gitconfig} | base64 --decode" 17 | 18 | # Test that the resources from https://github.com/fluxcd/flux-get-started are deployed 19 | poll_until_true 'namespace demo' 'kubectl describe ns/demo' 20 | poll_until_true 'workload podinfo' 'kubectl -n demo describe deployment/podinfo' 21 | } 22 | 23 | function teardown() { 24 | # Removing Flux also takes care of the global resources it installs. 25 | uninstall_flux_with_helm 26 | uninstall_tiller 27 | # Removing the namespace also takes care of removing gitsrv. 28 | kubectl delete namespace "$FLUX_NAMESPACE" 29 | # Only remove the demo workloads after Flux, so that they cannot be recreated. 30 | kubectl delete namespace "$DEMO_NAMESPACE" 31 | } 32 | -------------------------------------------------------------------------------- /test/e2e/11_fluxctl_install.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | function setup() { 4 | load lib/env 5 | load lib/install 6 | load lib/poll 7 | 8 | kubectl create namespace "$FLUX_NAMESPACE" 9 | install_git_srv 10 | install_flux_with_fluxctl 11 | } 12 | 13 | @test "'fluxctl install' smoke test" { 14 | # Test that the resources from https://github.com/fluxcd/flux-get-started are deployed 15 | poll_until_true 'namespace demo' 'kubectl describe ns/demo' 16 | poll_until_true 'workload podinfo' 'kubectl -n demo describe deployment/podinfo' 17 | } 18 | 19 | function teardown() { 20 | # Although the namespace delete below takes care of removing most Flux 21 | # elements, the global resources will not be removed without this. 22 | uninstall_flux_with_fluxctl 23 | # Removing the namespace also takes care of removing Flux and gitsrv. 24 | kubectl delete namespace "$FLUX_NAMESPACE" 25 | # Only remove the demo workloads after Flux, so that they cannot be recreated. 26 | kubectl delete namespace "$DEMO_NAMESPACE" 27 | } 28 | -------------------------------------------------------------------------------- /test/e2e/13_sync_gc.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | function setup() { 4 | load lib/env 5 | load lib/install 6 | load lib/poll 7 | load lib/defer 8 | 9 | kubectl create namespace "$FLUX_NAMESPACE" 10 | # Install flux and the git server, allowing external access 11 | install_git_srv git_srv_result 12 | # shellcheck disable=SC2154 13 | export GIT_SSH_COMMAND="${git_srv_result[0]}" 14 | # Teardown the created port-forward to gitsrv. 15 | defer kill "${git_srv_result[1]}" 16 | install_flux_with_fluxctl "13_sync_gc" 17 | } 18 | 19 | @test "Sync with garbage collection test" { 20 | # Wait until flux deploys the workloads, which indicates it has at least started a sync 21 | poll_until_true 'workload podinfo' 'kubectl -n demo describe deployment/podinfo' 22 | 23 | # make sure we have _finished_ a sync run 24 | fluxctl --k8s-fwd-ns "${FLUX_NAMESPACE}" sync 25 | 26 | # Clone the repo and check the sync tag 27 | local clone_dir 28 | clone_dir="$(mktemp -d)" 29 | defer rm -rf "'$clone_dir'" 30 | git clone -b master ssh://git@localhost/git-server/repos/cluster.git "$clone_dir" 31 | cd "$clone_dir" 32 | head_hash=$(git rev-list -n 1 HEAD) 33 | poll_until_equals "sync tag" "$head_hash" 'git pull -f --tags > /dev/null 2>&1; git rev-list -n 1 flux' 34 | 35 | # Remove a manifest and commit that 36 | git rm workloads/podinfo-dep.yaml 37 | git -c 'user.email=foo@bar.com' -c 'user.name=Foo' commit -m "Remove podinfo deployment" 38 | head_hash=$(git rev-list -n 1 HEAD) 39 | git push >&3 40 | 41 | fluxctl --k8s-fwd-ns "${FLUX_NAMESPACE}" sync 42 | 43 | poll_until_equals "podinfo deployment removed" "[]" "kubectl get deploy -n demo -o\"jsonpath={['items']}\"" 44 | poll_until_equals "sync tag" "$head_hash" 'git pull -f --tags > /dev/null 2>&1; git rev-list -n 1 flux' 45 | } 46 | 47 | function teardown() { 48 | run_deferred 49 | # Although the namespace delete below takes care of removing most Flux 50 | # elements, the global resources will not be removed without this. 51 | uninstall_flux_with_fluxctl 52 | # Removing the namespace also takes care of removing Flux and gitsrv. 53 | kubectl delete namespace "$FLUX_NAMESPACE" 54 | # Only remove the demo workloads after Flux, so that they cannot be recreated. 55 | kubectl delete namespace "$DEMO_NAMESPACE" 56 | } 57 | -------------------------------------------------------------------------------- /test/e2e/16_fluxctl_sync.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | clone_dir="" 4 | 5 | function setup() { 6 | load lib/env 7 | load lib/install 8 | load lib/poll 9 | load lib/defer 10 | 11 | kubectl create namespace "$FLUX_NAMESPACE" 12 | # Install flux and the git server, allowing external access 13 | install_git_srv git_srv_result 14 | # shellcheck disable=SC2154 15 | export GIT_SSH_COMMAND="${git_srv_result[0]}" 16 | # Teardown the created port-forward to gitsrv and restore Git settings. 17 | defer kill "${git_srv_result[1]}" 18 | 19 | install_flux_with_fluxctl '15_fluxctl_sync' 20 | 21 | # Clone the repo 22 | clone_dir="$(mktemp -d)" 23 | defer rm -rf "'$clone_dir'" 24 | git clone -b master ssh://git@localhost/git-server/repos/cluster.git "$clone_dir" 25 | # shellcheck disable=SC2164 26 | cd "$clone_dir" 27 | } 28 | 29 | @test "fluxctl sync" { 30 | 31 | # Sync 32 | poll_until_true 'fluxctl sync succeeds' "fluxctl --k8s-fwd-ns ${FLUX_NAMESPACE} sync" 33 | 34 | # Wait until flux deploys the workloads 35 | poll_until_true 'workload podinfo' 'kubectl -n demo describe deployment/podinfo' 36 | 37 | # Check the sync tag 38 | local head_hash 39 | head_hash=$(git rev-list -n 1 HEAD) 40 | poll_until_equals "sync tag" "$head_hash" 'git pull -f --tags > /dev/null 2>&1; git rev-list -n 1 flux' 41 | 42 | } 43 | 44 | function teardown() { 45 | run_deferred 46 | # Although the namespace delete below takes care of removing most Flux 47 | # elements, the global resources will not be removed without this. 48 | uninstall_flux_with_fluxctl 49 | # Removing the namespace also takes care of removing gitsrv. 50 | kubectl delete namespace "$FLUX_NAMESPACE" 51 | # Only remove the demo workloads after Flux, so that they cannot be recreated. 52 | kubectl delete namespace "$DEMO_NAMESPACE" 53 | } 54 | -------------------------------------------------------------------------------- /test/e2e/21_ssh_key_generation.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | function setup() { 4 | load lib/env 5 | load lib/install 6 | load lib/defer 7 | 8 | kubectl create namespace "$FLUX_NAMESPACE" 9 | 10 | # Installing Flux with the defaults _without_ installing the git 11 | # server (which generates an SSH key used by the server and Flux) 12 | # will cause Flux to generate an SSH key. 13 | install_flux_with_fluxctl 14 | } 15 | 16 | @test "SSH key is generated" { 17 | run fluxctl identity --k8s-fwd-ns "$FLUX_NAMESPACE" 18 | [ "$status" -eq 0 ] 19 | [ "$output" != "" ] 20 | 21 | fingerprint=$(echo "$output" | ssh-keygen -E md5 -lf - | awk '{ print $2 }') 22 | [ "$fingerprint" = "MD5:$(fluxctl identity -l --k8s-fwd-ns "$FLUX_NAMESPACE")" ] 23 | } 24 | 25 | function teardown() { 26 | run_deferred 27 | # Although the namespace delete below takes care of removing most Flux 28 | # elements, the global resources will not be removed without this. 29 | uninstall_flux_with_fluxctl 30 | # Removing the namespace also takes care of removing Flux and gitsrv. 31 | kubectl delete namespace "$FLUX_NAMESPACE" 32 | } 33 | -------------------------------------------------------------------------------- /test/e2e/22_manifest_generation.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | function setup() { 4 | load lib/env 5 | load lib/install 6 | load lib/poll 7 | load lib/defer 8 | 9 | kubectl create namespace "$FLUX_NAMESPACE" 10 | # Install flux and the git server, allowing external access 11 | install_git_srv git_srv_result "22_manifest_generation/gitsrv" 12 | # shellcheck disable=SC2154 13 | export GIT_SSH_COMMAND="${git_srv_result[0]}" 14 | # Teardown the created port-forward to gitsrv. 15 | defer kill "${git_srv_result[1]}" 16 | install_flux_with_fluxctl "22_manifest_generation/flux" 17 | } 18 | 19 | @test "Basic sync and editing" { 20 | # Wait until flux deploys the workloads 21 | poll_until_true 'workload podinfo' 'kubectl -n demo describe deployment/podinfo' 22 | 23 | # Make sure that the production patch is applied (the podinfo HorizontalPodAutoscaler should have 24 | # a minReplicas value of 2) 25 | poll_until_equals 'podinfo hpa minReplicas of 2' '2' "kubectl get hpa podinfo --namespace demo -o\"jsonpath={['spec']['minReplicas']}\"" 26 | 27 | # Make sure the 'patchUpdated' mechanism works when changing annotations through fluxctl 28 | fluxctl --k8s-fwd-ns "${FLUX_NAMESPACE}" automate -n demo --workload deployment/podinfo >&3 29 | 30 | poll_until_true 'podinfo to be automated' "fluxctl --k8s-fwd-ns \"${FLUX_NAMESPACE}\" list-workloads -n demo | grep podinfod | grep automated" 31 | 32 | } 33 | 34 | function teardown() { 35 | run_deferred 36 | # Although the namespace delete below takes care of removing most Flux 37 | # elements, the global resources will not be removed without this. 38 | uninstall_flux_with_fluxctl 39 | # Removing the namespace also takes care of removing Flux and gitsrv. 40 | kubectl delete namespace "$FLUX_NAMESPACE" 41 | # Only remove the demo workloads after Flux, so that they cannot be recreated. 42 | kubectl delete namespace "$DEMO_NAMESPACE" 43 | } 44 | -------------------------------------------------------------------------------- /test/e2e/fixtures/crane_empty_img_tmpl/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "architecture": "amd64", 3 | "config": { 4 | "Hostname": "", 5 | "Domainname": "", 6 | "User": "", 7 | "AttachStdin": false, 8 | "AttachStdout": false, 9 | "AttachStderr": false, 10 | "Tty": false, 11 | "OpenStdin": false, 12 | "StdinOnce": false, 13 | "Env": [ 14 | "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", 15 | "foo=" 16 | ], 17 | "Cmd": null, 18 | "Image": "", 19 | "Volumes": null, 20 | "WorkingDir": "", 21 | "Entrypoint": null, 22 | "OnBuild": null, 23 | "Labels": null 24 | }, 25 | "container": "622070b2ab6961b448c41a0ca3f694e7301ceaef4676aafc0f5302500a17300d", 26 | "container_config": { 27 | "Hostname": "622070b2ab69", 28 | "Domainname": "", 29 | "User": "", 30 | "AttachStdin": false, 31 | "AttachStdout": false, 32 | "AttachStderr": false, 33 | "Tty": false, 34 | "OpenStdin": false, 35 | "StdinOnce": false, 36 | "Env": [ 37 | "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" 38 | ], 39 | "Cmd": [ 40 | "/bin/sh", 41 | "-c", 42 | "#(nop) ", 43 | "ENV foo=" 44 | ], 45 | "Image": "", 46 | "Volumes": null, 47 | "WorkingDir": "", 48 | "Entrypoint": null, 49 | "OnBuild": null, 50 | "Labels": { 51 | "org.opencontainers.image.created": "${CREATION_TIME}", 52 | "org.label-schema.build-date": "${CREATION_TIME}" 53 | } 54 | }, 55 | "created": "${CREATION_TIME}", 56 | "docker_version": "19.03.5", 57 | "history": [ 58 | { 59 | "created": "${CREATION_TIME}", 60 | "created_by": "/bin/sh -c #(nop) ENV foo=", 61 | "empty_layer": true 62 | } 63 | ], 64 | "os": "linux", 65 | "rootfs": { 66 | "type": "layers" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /test/e2e/fixtures/crane_empty_img_tmpl/manifest.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Config": "config.json" 4 | } 5 | ] 6 | -------------------------------------------------------------------------------- /test/e2e/fixtures/gitconfig: -------------------------------------------------------------------------------- 1 | [core] 2 | editor = vim 3 | -------------------------------------------------------------------------------- /test/e2e/fixtures/kustom/13_sync_gc/gc_patch.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /spec/template/spec/containers/0/args/- 3 | value: --sync-garbage-collection 4 | -------------------------------------------------------------------------------- /test/e2e/fixtures/kustom/13_sync_gc/kustomization.yaml: -------------------------------------------------------------------------------- 1 | bases: 2 | - "../base/flux" 3 | patchesJson6902: 4 | - target: 5 | group: apps 6 | version: v1 7 | kind: Deployment 8 | name: flux 9 | path: gc_patch.yaml 10 | -------------------------------------------------------------------------------- /test/e2e/fixtures/kustom/14_release_image/kustomization.yaml: -------------------------------------------------------------------------------- 1 | bases: 2 | - "../base/flux" 3 | patchesJson6902: 4 | - target: 5 | group: apps 6 | version: v1 7 | kind: Deployment 8 | name: flux 9 | path: release_image_patch.yaml 10 | -------------------------------------------------------------------------------- /test/e2e/fixtures/kustom/14_release_image/release_image_patch.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /spec/template/spec/containers/0/args/- 3 | value: --registry-disable-scanning=false 4 | - op: add 5 | path: /spec/template/spec/containers/0/args/- 6 | value: --registry-exclude-image=*bitnami/mongodb,*bitnami/redis,*k8s.gcr.io*,*docker/kube-*,*fluxcd/flux,*alpine,*memcached,*stefanprodan/gitsrv,*registry 7 | # replace docker's registry by our local one 8 | - op: add 9 | path: /spec/template/spec/containers/0/args/- 10 | value: --registry-insecure-host=index.docker.io 11 | - op: add 12 | path: /spec/template/spec/hostAliases 13 | value: 14 | - ip: "${REGISTRY_SERVICE_IP}" 15 | hostnames: 16 | - "index.docker.io" 17 | # Make automation fast so that we don't need to wait too much 18 | - op: add 19 | path: /spec/template/spec/containers/0/args/- 20 | value: --automation-interval=5s 21 | -------------------------------------------------------------------------------- /test/e2e/fixtures/kustom/15_fluxctl_sync/fluxctl_sync.yaml: -------------------------------------------------------------------------------- 1 | # make sure automatic syncs don't kick in 2 | - op: add 3 | path: /spec/template/spec/containers/0/args/- 4 | value: --sync-interval=525600m 5 | - op: add 6 | path: /spec/template/spec/containers/0/args/- 7 | value: --git-poll-interval=525600m 8 | -------------------------------------------------------------------------------- /test/e2e/fixtures/kustom/15_fluxctl_sync/kustomization.yaml: -------------------------------------------------------------------------------- 1 | bases: 2 | - "../base/flux" 3 | patchesJson6902: 4 | - target: 5 | group: apps 6 | version: v1 7 | kind: Deployment 8 | name: flux 9 | path: fluxctl_sync.yaml 10 | -------------------------------------------------------------------------------- /test/e2e/fixtures/kustom/20_gpg/flux/gpg_patch.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /spec/template/spec/containers/0/args/- 3 | value: --git-verify-signatures=$FLUX_GIT_VERIFY_SIGNATURES 4 | - op: add 5 | path: /spec/template/spec/containers/0/args/- 6 | value: --git-signing-key=$FLUX_GPG_KEY_ID 7 | - op: add 8 | path: /spec/template/spec/containers/0/args/- 9 | value: --git-gpg-key-import=/root/gpg-import/private 10 | - op: add 11 | path: /spec/template/spec/containers/0/volumeMounts/- 12 | value: 13 | name: gpg-keys 14 | mountPath: /root/gpg-import/private 15 | readOnly: true 16 | - op: add 17 | path: /spec/template/spec/volumes/- 18 | value: 19 | name: gpg-keys 20 | secret: 21 | secretName: flux-gpg-signing-key 22 | defaultMode: 0400 23 | -------------------------------------------------------------------------------- /test/e2e/fixtures/kustom/20_gpg/flux/kustomization.yaml: -------------------------------------------------------------------------------- 1 | bases: 2 | - "../../base/flux" 3 | patchesJson6902: 4 | - target: 5 | group: apps 6 | version: v1 7 | kind: Deployment 8 | name: flux 9 | path: gpg_patch.yaml 10 | -------------------------------------------------------------------------------- /test/e2e/fixtures/kustom/20_gpg/gitsrv/gpg_patch.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /spec/template/spec/containers/0/env/- 3 | value: 4 | name: GPG_KEYFILE 5 | value: /git-server/gpg/flux.asc 6 | - op: add 7 | path: /spec/template/spec/containers/0/volumeMounts/- 8 | value: 9 | mountPath: /git-server/gpg 10 | name: git-gpg-keys 11 | - op: add 12 | path: /spec/template/spec/volumes/- 13 | value: 14 | name: git-gpg-keys 15 | secret: 16 | secretName: flux-gpg-signing-key 17 | -------------------------------------------------------------------------------- /test/e2e/fixtures/kustom/20_gpg/gitsrv/kustomization.yaml: -------------------------------------------------------------------------------- 1 | bases: 2 | - "../../base/gitsrv" 3 | patchesJson6902: 4 | - target: 5 | group: apps 6 | version: v1 7 | kind: Deployment 8 | name: gitsrv 9 | path: gpg_patch.yaml 10 | -------------------------------------------------------------------------------- /test/e2e/fixtures/kustom/22_manifest_generation/flux/kustomization.yaml: -------------------------------------------------------------------------------- 1 | bases: 2 | - "../../base/flux" 3 | patchesJson6902: 4 | - target: 5 | group: apps 6 | version: v1 7 | kind: Deployment 8 | name: flux 9 | path: manifest_generation_patch.yaml 10 | -------------------------------------------------------------------------------- /test/e2e/fixtures/kustom/22_manifest_generation/flux/manifest_generation_patch.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /spec/template/spec/containers/0/args/- 3 | value: --manifest-generation 4 | - op: add 5 | path: /spec/template/spec/containers/0/args/- 6 | value: --git-path=production 7 | -------------------------------------------------------------------------------- /test/e2e/fixtures/kustom/22_manifest_generation/gitsrv/kustomization.yaml: -------------------------------------------------------------------------------- 1 | bases: 2 | - "../../base/gitsrv" 3 | patchesJson6902: 4 | - target: 5 | group: apps 6 | version: v1 7 | kind: Deployment 8 | name: gitsrv 9 | path: manifest_generation_patch.yaml 10 | -------------------------------------------------------------------------------- /test/e2e/fixtures/kustom/22_manifest_generation/gitsrv/manifest_generation_patch.yaml: -------------------------------------------------------------------------------- 1 | - op: replace 2 | path: /spec/template/spec/containers/0/env 3 | value: 4 | - name: REPO 5 | value: "cluster.git" 6 | - name: TAR_URL 7 | value: https://github.com/weaveworks/flux-kustomize-example/archive/master.tar.gz 8 | -------------------------------------------------------------------------------- /test/e2e/fixtures/kustom/base/flux/e2e_patch.yaml: -------------------------------------------------------------------------------- 1 | - op: replace 2 | path: /spec/template/spec/containers/0/image 3 | value: docker.io/fluxcd/flux:latest # test the flux version being developed on and not the latest 4 | # release 5 | - op: add 6 | path: /spec/template/spec/containers/0/args/- 7 | value: --git-poll-interval=10s 8 | - op: add 9 | path: /spec/template/spec/containers/0/args/- 10 | value: --sync-interval=10s 11 | - op: add 12 | path: /spec/template/spec/containers/0/args/- 13 | value: --registry-disable-scanning 14 | - op: add 15 | path: /spec/template/spec/containers/0/volumeMounts/- 16 | value: 17 | name: known-hosts 18 | mountPath: /root/.ssh/known_hosts 19 | subPath: known_hosts 20 | - op: add 21 | path: /spec/template/spec/volumes/- 22 | value: 23 | name: known-hosts 24 | configMap: 25 | name: flux-known-hosts 26 | -------------------------------------------------------------------------------- /test/e2e/fixtures/kustom/base/flux/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - flux-account.yaml 3 | - flux-deployment.yaml 4 | - flux-secret.yaml 5 | - memcache-dep.yaml 6 | - memcache-svc.yaml 7 | patchesJson6902: 8 | - target: 9 | group: apps 10 | version: v1 11 | kind: Deployment 12 | name: flux 13 | path: e2e_patch.yaml 14 | -------------------------------------------------------------------------------- /test/e2e/fixtures/kustom/base/gitsrv/gitsrv.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | labels: 6 | name: gitsrv 7 | name: gitsrv 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | name: gitsrv 13 | template: 14 | metadata: 15 | labels: 16 | name: gitsrv 17 | spec: 18 | containers: 19 | - image: fluxcd/gitsrv:v1.0.0 20 | name: git 21 | env: 22 | - name: REPO 23 | value: "cluster.git" 24 | - name: TAR_URL 25 | value: "https://github.com/fluxcd/flux-get-started/archive/a4bdf4bb92c35cf9e944788368510168ee0bedf4.tar.gz" 26 | ports: 27 | - containerPort: 22 28 | name: ssh 29 | protocol: TCP 30 | readinessProbe: 31 | tcpSocket: 32 | port: 22 33 | initialDelaySeconds: 20 34 | periodSeconds: 10 35 | livenessProbe: 36 | tcpSocket: 37 | port: 22 38 | initialDelaySeconds: 20 39 | periodSeconds: 10 40 | volumeMounts: 41 | - mountPath: /git-server/repos 42 | name: git-server-data 43 | - mountPath: /git-server/keys 44 | name: flux-git-deploy 45 | volumes: 46 | - name: flux-git-deploy 47 | secret: 48 | secretName: flux-git-deploy 49 | - name: git-server-data 50 | emptyDir: {} 51 | --- 52 | apiVersion: v1 53 | kind: Service 54 | metadata: 55 | labels: 56 | name: gitsrv 57 | name: gitsrv 58 | spec: 59 | ports: 60 | - name: ssh 61 | port: 22 62 | protocol: TCP 63 | targetPort: ssh 64 | selector: 65 | name: gitsrv 66 | type: ClusterIP 67 | -------------------------------------------------------------------------------- /test/e2e/fixtures/kustom/base/gitsrv/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - gitsrv.yaml 3 | -------------------------------------------------------------------------------- /test/e2e/fixtures/kustom/base/registry/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - registry.yaml 3 | -------------------------------------------------------------------------------- /test/e2e/fixtures/kustom/base/registry/registry.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: registry 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | name: registry 11 | template: 12 | metadata: 13 | labels: 14 | name: registry 15 | spec: 16 | containers: 17 | - image: registry:2 18 | name: registry 19 | --- 20 | apiVersion: v1 21 | kind: Service 22 | metadata: 23 | name: registry 24 | spec: 25 | ports: 26 | - name: http 27 | port: 80 28 | protocol: TCP 29 | targetPort: 5000 30 | selector: 31 | name: registry 32 | type: ClusterIP 33 | -------------------------------------------------------------------------------- /test/e2e/lib/defer.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This lets you call `defer` to record an action to do later; 4 | # `run_deferred` should be called in an EXIT trap, either explicitly: 5 | # 6 | # trap run_deferred EXIT 7 | # 8 | # or when using with tests, by calling it in the teardown function 9 | # (which bats will arrange to run). 10 | 11 | declare -a on_exit_items 12 | 13 | function run_deferred() { 14 | if [ "${#on_exit_items[@]}" -gt 0 ]; then 15 | echo -e '\nRunning deferred items, please do not interrupt until they are done:' 16 | fi 17 | for I in "${on_exit_items[@]}"; do 18 | echo "deferred: ${I}" 19 | eval "${I}" 20 | done 21 | } 22 | 23 | function defer() { 24 | on_exit_items=("$*" "${on_exit_items[@]}") 25 | } 26 | -------------------------------------------------------------------------------- /test/e2e/lib/env.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export FLUX_NAMESPACE=flux-e2e 4 | export DEMO_NAMESPACE=demo 5 | FLUX_ROOT_DIR=$(git rev-parse --show-toplevel) 6 | export FLUX_ROOT_DIR 7 | export E2E_DIR="${FLUX_ROOT_DIR}/test/e2e" 8 | export FIXTURES_DIR="${E2E_DIR}/fixtures" 9 | KNOWN_HOSTS=$(cat "${FLUX_ROOT_DIR}/cache/known_hosts") 10 | export KNOWN_HOSTS 11 | GITCONFIG=$(cat "${FIXTURES_DIR}/gitconfig") 12 | export GITCONFIG 13 | 14 | # Wire the test to the right cluster when tests are run in parallel 15 | if eval [ -n '$KUBECONFIG_SLOT_'"${BATS_JOB_SLOT}" ]; then 16 | eval export KUBECONFIG='$KUBECONFIG_SLOT_'"${BATS_JOB_SLOT}" 17 | fi 18 | -------------------------------------------------------------------------------- /test/e2e/lib/gpg.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function create_gpg_key() { 4 | local name=${1:-Flux} 5 | local email=${2:-support@weave.works} 6 | 7 | # https://www.gnupg.org/documentation/manuals/gnupg-devel/Unattended-GPG-key-generation.html 8 | local batchcfg 9 | batchcfg=$(mktemp) 10 | 11 | cat > "$batchcfg" << EOF 12 | %echo Generating a throwaway OpenPGP key for "$name <$email>" 13 | Key-Type: 1 14 | Key-Length: 2048 15 | Subkey-Type: 1 16 | Subkey-Length: 2048 17 | Name-Real: $name 18 | Name-Email: $email 19 | Expire-Date: 0 20 | %no-protection 21 | %commit 22 | %echo Done 23 | EOF 24 | 25 | # Generate the key with the written config 26 | gpg --batch --gen-key "$batchcfg" 27 | rm "$batchcfg" 28 | 29 | # Find the ID of the key we just generated 30 | local key_id 31 | key_id=$(gpg --no-tty --list-secret-keys --with-colons "$name" 2> /dev/null | 32 | awk -F: '/^sec:/ { print $5 }' | tail -1) 33 | echo "$key_id" 34 | } 35 | 36 | function create_secret_from_gpg_key() { 37 | local key_id="${1}" 38 | 39 | if [ -z "$key_id" ]; then 40 | echo "no key ID provided" >&2 41 | exit 1 42 | fi 43 | 44 | # Export key to secret 45 | gpg --export-secret-keys "$key_id" | 46 | kubectl --namespace "${FLUX_NAMESPACE}" \ 47 | create secret generic flux-gpg-signing-key \ 48 | --from-file=flux.asc=/dev/stdin 49 | } 50 | -------------------------------------------------------------------------------- /test/e2e/lib/poll.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function poll_until_equals() { 4 | local what="$1" 5 | local expected="$2" 6 | local check_cmd="$3" 7 | local retries="$4" 8 | local wait_period="$5" 9 | poll_until_true "$what" "[ '$expected' = \"\$( $check_cmd )\" ]" "$retries" "$wait_period" 10 | } 11 | 12 | function poll_until_true() { 13 | local what="$1" 14 | local check_cmd="$2" 15 | # timeout after $retries * $wait_period seconds 16 | local retries=${3:-24} 17 | local wait_period=${4:-5} 18 | echo -n ">>> Waiting for $what " >&3 19 | count=0 20 | until eval "$check_cmd"; do 21 | echo -n '.' >&3 22 | sleep "$wait_period" 23 | count=$((count + 1)) 24 | if [[ ${count} -eq ${retries} ]]; then 25 | echo ' No more retries left!' >&3 26 | return 1 # fail 27 | fi 28 | done 29 | echo ' done' >&3 30 | } 31 | -------------------------------------------------------------------------------- /test/e2e/lib/registry.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # shellcheck disable=SC1090 4 | source "${E2E_DIR}/lib/defer.bash" 5 | # shellcheck disable=SC1090 6 | source "${E2E_DIR}/lib/template.bash" 7 | 8 | # pushes an empty image (layerless) to a given registry 9 | function push_empty_image() { 10 | local registry_host=$1 11 | local image_name_and_tag=$2 12 | local creation_time=$3 # Format: 2020-01-20T13:53:05.47178071Z 13 | 14 | image_dir=$(mktemp -d) 15 | defer rm -rf "$image_dir" 16 | cp "${E2E_DIR}/fixtures/crane_empty_img_tmpl/"* "${image_dir}/" 17 | local -A template_values 18 | # shellcheck disable=SC2034 19 | template_values['CREATION_TIME']="$creation_time" 20 | fill_in_place_recursively 'template_values' "$image_dir" 21 | 22 | tar -cf "${image_dir}/image.tar" -C "$image_dir" manifest.json config.json 23 | crane push "${image_dir}/image.tar" "${registry_host}/${image_name_and_tag}" 24 | } 25 | -------------------------------------------------------------------------------- /test/e2e/lib/template.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function fill_in_place_recursively() { 4 | local -n key_values=$1 # pass an associate array as a nameref 5 | local target_directory=${2:-.} 6 | (# use a subshell to expose key-values as variables for envsubst to use 7 | for key in "${!key_values[@]}"; do 8 | export "$key"="${key_values[$key]}" 9 | done 10 | # Use find with zero-ended strings and read to avoid problems 11 | # with spaces in paths 12 | while IFS= read -r -d '' file; do 13 | # Use a command group to ensure "$file" is not 14 | # deleted before being written to. 15 | # shellcheck disable=SC2094 16 | { 17 | rm "$file" 18 | envsubst > "$file" 19 | } < "$file" 20 | done < <(find "$target_directory" -type f -print0) 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /test/e2e/run-gh.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | 5 | # This script runs the bats tests 6 | 7 | # Directory paths we need to be aware of 8 | FLUX_ROOT_DIR="$(git rev-parse --show-toplevel)" 9 | E2E_DIR="${FLUX_ROOT_DIR}/test/e2e" 10 | CACHE_DIR="${FLUX_ROOT_DIR}/cache/$CURRENT_OS_ARCH" 11 | 12 | KIND_VERSION=v0.11.1 13 | KUBE_VERSION=v1.21.1 14 | GITSRV_VERSION=v1.0.0 15 | KIND_CACHE_PATH="${CACHE_DIR}/kind-$KIND_VERSION" 16 | KIND_CLUSTER_PREFIX=flux-e2e 17 | BATS_EXTRA_ARGS="" 18 | 19 | # shellcheck disable=SC1090 20 | source "${E2E_DIR}/lib/defer.bash" 21 | trap run_deferred EXIT 22 | 23 | mkdir -p "${FLUX_ROOT_DIR}/cache" 24 | curl -sL "https://github.com/fluxcd/gitsrv/releases/download/${GITSRV_VERSION}/known_hosts.txt" > "${FLUX_ROOT_DIR}/cache/known_hosts" 25 | 26 | echo '>>> Running the tests' 27 | # Run all tests by default but let users specify which ones to run, e.g. with E2E_TESTS='11_*' make e2e 28 | E2E_TESTS=${E2E_TESTS:-.} 29 | ( 30 | cd "${E2E_DIR}" 31 | # shellcheck disable=SC2086 32 | "${E2E_DIR}/bats/bin/bats" -t ${BATS_EXTRA_ARGS} ${E2E_TESTS} 33 | ) 34 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | // +build tools 2 | 3 | // This file just exists to ensure we download the tools we need for building 4 | // See https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module 5 | 6 | package flux 7 | 8 | import ( 9 | _ "github.com/google/go-containerregistry/cmd/crane" 10 | ) 11 | --------------------------------------------------------------------------------