From 6acdeeb605c20f8b146ac39ab391214bf9d872fd Mon Sep 17 00:00:00 2001 From: Carolyn Van Slyck Date: Mon, 14 Jun 2021 11:50:06 -0500 Subject: [PATCH 01/11] Install extra mixins used by examples Signed-off-by: Carolyn Van Slyck --- magefile.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/magefile.go b/magefile.go index b1adb038d..e2d23e8e1 100644 --- a/magefile.go +++ b/magefile.go @@ -82,7 +82,7 @@ func GetMixins() error { mixinTag = "canary" } - mixins := []string{"helm", "arm", "terraform", "kubernetes"} + mixins := []string{"docker", "docker-compose", "helm", "arm", "terraform", "kubernetes"} var errG errgroup.Group for _, mixin := range mixins { mixinDir := filepath.Join("bin/mixins/", mixin) From 0eb66181f2639fe90037ec18f61fab14acc56ad1 Mon Sep 17 00:00:00 2001 From: Carolyn Van Slyck Date: Mon, 14 Jun 2021 12:22:41 -0500 Subject: [PATCH 02/11] Support manually building examples during PR builds Allow a maintainer to kick a build of porter that includes building the example bundles. Signed-off-by: Carolyn Van Slyck --- build/azure-pipelines.pr-automatic.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/build/azure-pipelines.pr-automatic.yml b/build/azure-pipelines.pr-automatic.yml index 0e6f40ced..8f9d3cbfd 100644 --- a/build/azure-pipelines.pr-automatic.yml +++ b/build/azure-pipelines.pr-automatic.yml @@ -13,6 +13,11 @@ pool: variables: GOVERSION: "1.13.10" +parameters: + - name: buildExamples + type: boolean + default: false + stages: - stage: Setup jobs: @@ -74,8 +79,8 @@ stages: displayName: "Verify" - bash: make test-unit displayName: "Unit Test" - - job: build_examples - displayName: "Build Examples" + - job: validate_examples + displayName: "Validate Examples" dependsOn: build steps: - task: DownloadPipelineArtifact@2 @@ -88,6 +93,9 @@ stages: displayName: "Setup Bin" - bash: make lint-examples displayName: "Lint Examples" + - bash: make build-examples + displayName: "Build Examples" + condition: ${{parameters.buildExamples}} - job: build_docker dependsOn: xbuild steps: From 976b4b621377c5236874e4a13938cb3953fcb8ed Mon Sep 17 00:00:00 2001 From: Carolyn Van Slyck Date: Tue, 15 Jun 2021 11:36:04 -0500 Subject: [PATCH 03/11] Skip publish for v1 canary and latest (#1641) * Change permalinks to prefix with canary/latest I was running into conflict with how we detect versions with the other tagging scheme (v1-latest would get flagged as a real release). Switching the order so that it's latest-v1 will help avoid a bunch of false positives and workarounds. * Only publish v*, latest and canary Do not publish latest-* or canary-* for simplicity Signed-off-by: Carolyn Van Slyck --- build/azure-pipelines.release.yml | 9 +- .../content/project/version-strategy/index.md | 2 +- mage/git.go | 15 ++-- mage/releases/publish.go | 33 ++++--- magefile.go | 86 ++++++++++++++++--- pkg/pkgmgmt/feed/generate.go | 5 +- pkg/pkgmgmt/feed/generate_test.go | 16 +--- pkg/pkgmgmt/feed/testdata/atom-existing.xml | 11 --- pkg/pkgmgmt/feed/testdata/atom.xml | 10 --- scripts/build-images.sh | 14 --- scripts/publish-images.sh | 20 ----- 11 files changed, 120 insertions(+), 101 deletions(-) delete mode 100755 scripts/build-images.sh delete mode 100755 scripts/publish-images.sh diff --git a/build/azure-pipelines.release.yml b/build/azure-pipelines.release.yml index bc275f42b..7118d2151 100644 --- a/build/azure-pipelines.release.yml +++ b/build/azure-pipelines.release.yml @@ -1,7 +1,12 @@ trigger: - branches: + tags: include: - - refs/tags/v* + - "v*" + exclude: + # We tag a release for canary-v1 or latest-v1 because of how we host our binaries with GitHub releases. + # Do not trigger another release when we create these tags during the release process, preventing an infinite recursion of release builds. + - "latest*" + - "canary*" # Do not run on pull requests pr: none diff --git a/docs/content/project/version-strategy/index.md b/docs/content/project/version-strategy/index.md index 2711823e8..0d6b160d7 100644 --- a/docs/content/project/version-strategy/index.md +++ b/docs/content/project/version-strategy/index.md @@ -38,6 +38,6 @@ Porter v1 will include a number of breaking changes that we are grouping togethe The final release from the v1 branch will be v1.0.0. * The release/v1 branch will be merged into main, and then the v1.0.0 release is cut. * The latest and canary builds continue to be based on builds of the main branch only. - We may provide v1-latest and v1-canary builds at a later date. + We may provide latest-v1 and canary-v1 builds at a later date. [semver v2]: https://semver.org/spec/v2.0.0.html diff --git a/mage/git.go b/mage/git.go index b8115ac58..e92644c2c 100644 --- a/mage/git.go +++ b/mage/git.go @@ -31,6 +31,11 @@ type GitMetadata struct { IsTaggedRelease bool } +func (m GitMetadata) ShouldPublishPermalink() bool { + // For now don't publish canary-v1 or latest-v1 to keep things simpler + return m.Permalink == "canary" || m.Permalink == "latest" +} + // LoadMetadata populates the status of the current working copy: current version, tag and permalink func LoadMetadata() GitMetadata { loadMetadata.Do(func() { @@ -96,21 +101,21 @@ func getBranchName() string { func getPermalink() (string, bool) { // Use latest for tagged commits taggedRelease := false - permalinkSuffix := "canary" + permalinkPrefix := "canary" err := shx.RunS("git", "describe", "--tags", "--match=v*", "--exact") if err == nil { - permalinkSuffix = "latest" + permalinkPrefix = "latest" taggedRelease = true } // Get the current branch name, or the name of the branch we tagged from branch := getBranchName() - // Build a permalink such as "canary", "latest", "v1-latest", etc + // Build a permalink such as "canary", "latest", "latest-v1", or "canary-v1" switch branch { case "main": - return permalinkSuffix, taggedRelease + return permalinkPrefix, taggedRelease default: - return fmt.Sprintf("%s-%s", strings.TrimPrefix(branch, "release/"), permalinkSuffix), taggedRelease + return fmt.Sprintf("%s-%s", permalinkPrefix, strings.TrimPrefix(branch, "release/")), taggedRelease } } diff --git a/mage/releases/publish.go b/mage/releases/publish.go index 170843ec3..2afedd4b3 100644 --- a/mage/releases/publish.go +++ b/mage/releases/publish.go @@ -6,6 +6,7 @@ import ( "log" "os" "path/filepath" + "strings" "get.porter.sh/porter/mage" "get.porter.sh/porter/mage/tools" @@ -95,15 +96,19 @@ func publishPackage(pkgType string, name string) { remote := fmt.Sprintf("https://%s.git", repo) versionDir := filepath.Join("bin", pkgType+"s", name, info.Version) - // Move the permalink tag. The existing release automatically points to the tag. - must.RunV("git", "tag", info.Permalink, info.Version+"^{}", "-f") - must.RunV("git", "push", "-f", remote, info.Permalink) - // Create or update GitHub release for the permalink (canary/latest) with the version's binaries - AddFilesToRelease(repo, info.Permalink, versionDir) + if info.ShouldPublishPermalink() { + // Move the permalink tag. The existing release automatically points to the tag. + must.RunV("git", "tag", info.Permalink, info.Version+"^{}", "-f") + must.RunV("git", "push", "-f", remote, info.Permalink) + + AddFilesToRelease(repo, info.Permalink, versionDir) + } else { + fmt.Println("Skipping publish package for permalink", info.Permalink) + } + // Create GitHub release for the exact version (v1.2.3) and attach assets if info.IsTaggedRelease { - // Create GitHub release for the exact version (v1.2.3) and attach assets AddFilesToRelease(repo, info.Version, versionDir) } } @@ -116,12 +121,16 @@ func PublishMixin(mixin string) { // Publish a plugin's binaries. func PublishPlugin(plugin string) { publishPackage("plugin", plugin) - } func publishPackageFeed(pkgType string, name string) { info := mage.LoadMetadata() + if !info.ShouldPublishPermalink() { + fmt.Println("Skipping publish package feed for permalink", info.Permalink) + return + } + // Clone the packages repository if _, err := os.Stat(packagesRepo); !os.IsNotExist(err) { os.RemoveAll(packagesRepo) @@ -168,20 +177,20 @@ func GeneratePluginFeed() { // AddFilesToRelease uploads the files in the specified directory to a GitHub release. // If the release does not exist already, it will be created with empty release notes. -func AddFilesToRelease(repo string, version string, dir string) { +func AddFilesToRelease(repo string, tag string, dir string) { files := listFiles(dir) // Mark canary releases as a draft draft := "" - if version == "canary" { + if strings.HasPrefix(tag, "canary") { draft = "-p" } - if releaseExists(repo, version) { - must.Command("gh", "release", "upload", "--clobber", "-R", repo, version). + if releaseExists(repo, tag) { + must.Command("gh", "release", "upload", "--clobber", "-R", repo, tag). Args(files...).RunV() } else { - must.Command("gh", "release", "create", "-R", repo, "-t", version, "--notes=", draft, version). + must.Command("gh", "release", "create", "-R", repo, "-t", tag, "--notes=", draft, tag). CollapseArgs().Args(files...).RunV() } } diff --git a/magefile.go b/magefile.go index e2d23e8e1..5ed0d1d52 100644 --- a/magefile.go +++ b/magefile.go @@ -148,24 +148,86 @@ func getDualPublish() bool { func BuildImages() { info := mage.LoadMetadata() + registry := getRegistry() - must.Command("./scripts/build-images.sh").Env("VERSION="+info.Version, "PERMALINK="+info.Permalink, "REGISTRY="+getRegistry()).RunV() + buildImages(registry, info) if getDualPublish() { - must.Command("./scripts/build-images.sh").Env("VERSION="+info.Version, "PERMALINK="+info.Permalink, "REGISTRY=ghcr.io/getporter").RunV() + buildImages("ghcr.io/getporter", info) } } +func buildImages(registry string, info mage.GitMetadata) { + var g errgroup.Group + + g.Go(func() error { + img := fmt.Sprintf("%s/porter:%s", registry, info.Version) + err := shx.RunV("docker", "build", "-t", img, "-f", "build/images/client/Dockerfile", ".") + if err != nil { + return err + } + + err = shx.Run("docker", "tag", img, fmt.Sprintf("%s/porter:%s", registry, info.Permalink)) + if err != nil { + return err + } + + // porter-agent does a FROM porter so they can't go in parallel + img = fmt.Sprintf("%s/porter-agent:%s", registry, info.Version) + err = shx.RunV("docker", "build", "-t", img, "--build-arg", "PORTER_VERSION="+info.Version, "--build-arg", "REGISTRY="+registry, "-f", "build/images/agent/Dockerfile", "build/images/agent") + if err != nil { + return err + } + + return shx.Run("docker", "tag", img, fmt.Sprintf("%s/porter-agent:%s", registry, info.Permalink)) + }) + + g.Go(func() error { + img := fmt.Sprintf("%s/workshop:%s", registry, info.Version) + err := shx.RunV("docker", "build", "-t", img, "-f", "build/images/workshop/Dockerfile", ".") + if err != nil { + return err + } + + return shx.Run("docker", "tag", img, fmt.Sprintf("%s/workshop:%s", registry, info.Permalink)) + }) + + mgx.Must(g.Wait()) +} + func PublishImages() { mg.Deps(BuildImages) info := mage.LoadMetadata() - must.Command("./scripts/publish-images.sh").Env("VERSION="+info.Version, "PERMALINK="+info.Permalink, "REGISTRY="+getRegistry()).RunV() + pushImagesTo(getRegistry(), info) if getDualPublish() { - must.Command("./scripts/publish-images.sh").Env("VERSION="+info.Version, "PERMALINK="+info.Permalink, "REGISTRY=ghcr.io/getporter").RunV() + pushImagesTo("ghcr.io/getporter", info) } } +// Only push tagged versions, canary and latest +func pushImagesTo(registry string, info mage.GitMetadata) { + if info.IsTaggedRelease { + pushImages(registry, info.Version) + } + + if info.ShouldPublishPermalink() { + pushImages(registry, info.Permalink) + } else { + fmt.Println("Skipping image publish for permalink", info.Permalink) + } +} + +func pushImages(registry string, tag string) { + pushImage(fmt.Sprintf("%s/porter:%s", registry, tag)) + pushImage(fmt.Sprintf("%s/porter-agent:%s", registry, tag)) + pushImage(fmt.Sprintf("%s/workshop:%s", registry, tag)) +} + +func pushImage(img string) { + must.RunV("docker", "push", img) +} + // Publish the porter binaries and install scripts. func PublishPorter() { mg.Deps(tools.EnsureGitHubClient, releases.ConfigureGitBot) @@ -183,13 +245,17 @@ func PublishPorter() { } remote := fmt.Sprintf("https://%s.git", repo) - // Move the permalink tag. The existing release automatically points to the tag. - must.RunV("git", "tag", info.Permalink, info.Version+"^{}", "-f") - must.RunV("git", "push", "-f", remote, info.Permalink) - // Create or update GitHub release for the permalink (canary/latest) with the version's assets (porter binaries, exec binaries and install scripts) - releases.AddFilesToRelease(repo, info.Permalink, porterVersionDir) - releases.AddFilesToRelease(repo, info.Permalink, execVersionDir) + if info.ShouldPublishPermalink() { + // Move the permalink tag. The existing release automatically points to the tag. + must.RunV("git", "tag", info.Permalink, info.Version+"^{}", "-f") + must.RunV("git", "push", "-f", remote, info.Permalink) + + releases.AddFilesToRelease(repo, info.Permalink, porterVersionDir) + releases.AddFilesToRelease(repo, info.Permalink, execVersionDir) + } else { + fmt.Println("Skipping publish binaries for permalink", info.Permalink) + } if info.IsTaggedRelease { // Create GitHub release for the exact version (v1.2.3) and attach assets diff --git a/pkg/pkgmgmt/feed/generate.go b/pkg/pkgmgmt/feed/generate.go index dc9397187..5a44aee95 100644 --- a/pkg/pkgmgmt/feed/generate.go +++ b/pkg/pkgmgmt/feed/generate.go @@ -5,7 +5,6 @@ import ( "os" "regexp" "sort" - "strings" "time" "get.porter.sh/porter/pkg/context" @@ -127,8 +126,8 @@ var versionRegex = regexp.MustCompile(`\d+-g[a-z0-9]+`) // As a safety measure, skip versions that shouldn't be put in the feed, we only want canary and tagged releases. func shouldPublishVersion(version string) bool { - if strings.HasSuffix(version, "canary") { - // Publish canary permalinks + // Publish canary permalinks, for now ignore canary-v1 + if version == "canary" { return true } diff --git a/pkg/pkgmgmt/feed/generate_test.go b/pkg/pkgmgmt/feed/generate_test.go index e02a469e8..b2c52c554 100644 --- a/pkg/pkgmgmt/feed/generate_test.go +++ b/pkg/pkgmgmt/feed/generate_test.go @@ -63,12 +63,9 @@ func TestGenerate(t *testing.T) { tc.FileSystem.Create("bin/latest/helm-linux-amd64") tc.FileSystem.Create("bin/latest/helm-windows-amd64.exe") - tc.FileSystem.Create("bin/v2-latest/helm-darwin-amd64") - tc.FileSystem.Create("bin/v2-latest/helm-linux-amd64") - tc.FileSystem.Create("bin/v2-latest/helm-windows-amd64.exe") - tc.FileSystem.Chtimes("bin/v2-latest/helm-darwin-amd64", up4, up4) - tc.FileSystem.Chtimes("bin/v2-latest/helm-linux-amd64", up4, up4) - tc.FileSystem.Chtimes("bin/v2-latest/helm-windows-amd64.exe", up4, up4) + tc.FileSystem.Create("bin/canary-v1/exec-darwin-amd64") + tc.FileSystem.Create("bin/canary-v1/exec-linux-amd64") + tc.FileSystem.Create("bin/canary-v1/exec-windows-amd64.exe") opts := GenerateOptions{ AtomFile: "atom.xml", @@ -230,13 +227,6 @@ func TestGenerate_RegenerateDoesNotCreateDuplicates(t *testing.T) { tc.FileSystem.Chtimes("bin/canary/exec-linux-amd64", up10, up10) tc.FileSystem.Chtimes("bin/canary/exec-windows-amd64.exe", up10, up10) - tc.FileSystem.Create("bin/v2-latest/helm-darwin-amd64") - tc.FileSystem.Create("bin/v2-latest/helm-linux-amd64") - tc.FileSystem.Create("bin/v2-latest/helm-windows-amd64.exe") - tc.FileSystem.Chtimes("bin/v2-latest/helm-darwin-amd64", up4, up4) - tc.FileSystem.Chtimes("bin/v2-latest/helm-linux-amd64", up4, up4) - tc.FileSystem.Chtimes("bin/v2-latest/helm-windows-amd64.exe", up4, up4) - opts := GenerateOptions{ AtomFile: "atom.xml", SearchDirectory: "bin", diff --git a/pkg/pkgmgmt/feed/testdata/atom-existing.xml b/pkg/pkgmgmt/feed/testdata/atom-existing.xml index c7f8facc3..643e5203d 100644 --- a/pkg/pkgmgmt/feed/testdata/atom-existing.xml +++ b/pkg/pkgmgmt/feed/testdata/atom-existing.xml @@ -8,17 +8,6 @@ https://porter.sh/mixins - - - https://cdn.porter.sh/mixins/v2-latest/helm - helm @ v2-latest - 2013-02-4T00:00:00Z - - v2-latest - - - - https://cdn.porter.sh/mixins/canary/exec exec @ canary diff --git a/pkg/pkgmgmt/feed/testdata/atom.xml b/pkg/pkgmgmt/feed/testdata/atom.xml index b9d7ca474..af35a4363 100644 --- a/pkg/pkgmgmt/feed/testdata/atom.xml +++ b/pkg/pkgmgmt/feed/testdata/atom.xml @@ -19,16 +19,6 @@ - - https://cdn.porter.sh/mixins/v2-latest/helm - helm @ v2-latest - 2013-02-04T00:00:00Z - - v2-latest - - - - https://cdn.porter.sh/mixins/v1.2.4/helm helm @ v1.2.4 diff --git a/scripts/build-images.sh b/scripts/build-images.sh deleted file mode 100755 index 0d692a683..000000000 --- a/scripts/build-images.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - - -# REGISTRY, PERMALINK and VERSION must be set before calling this script -# It is intended to only be executed by make publish - -docker build -t $REGISTRY/porter:$VERSION -f build/images/client/Dockerfile . -docker build -t $REGISTRY/porter-agent:$VERSION --build-arg PORTER_VERSION=$VERSION --build-arg REGISTRY=$REGISTRY -f build/images/agent/Dockerfile build/images/agent -docker build -t $REGISTRY/workshop:$VERSION -f build/images/workshop/Dockerfile . - -docker tag $REGISTRY/porter:$VERSION $REGISTRY/porter:$PERMALINK -docker tag $REGISTRY/porter-agent:$VERSION $REGISTRY/porter-agent:$PERMALINK -docker tag $REGISTRY/workshop:$VERSION $REGISTRY/workshop:$PERMALINK diff --git a/scripts/publish-images.sh b/scripts/publish-images.sh deleted file mode 100755 index 2bd1975d7..000000000 --- a/scripts/publish-images.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - - -# REGISTRY, PERMALINK and VERSION must be set before calling this script -# It is intended to only be executed by make publish - -if [[ "$PERMALINK" == *latest ]]; then - docker push $REGISTRY/porter:$VERSION - docker push $REGISTRY/porter-agent:$VERSION - docker push $REGISTRY/workshop:$VERSION - - docker push $REGISTRY/porter:$PERMALINK - docker push $REGISTRY/porter-agent:$PERMALINK - docker push $REGISTRY/workshop:$PERMALINK -else - docker push $REGISTRY/porter:$PERMALINK - docker push $REGISTRY/porter-agent:$PERMALINK - docker push $REGISTRY/workshop:$PERMALINK -fi From 20629c667efd14ecfc3048386bc4c1ac97403149 Mon Sep 17 00:00:00 2001 From: Carolyn Van Slyck Date: Tue, 15 Jun 2021 12:35:31 -0500 Subject: [PATCH 04/11] Allow tests to run in parallel Do not stop the docker registry during the smoke tests, since make test runs all 3 types of tests at once and that causes the registry to tear down in the middle of the integration tests. Signed-off-by: Carolyn Van Slyck --- magefile.go | 1 - 1 file changed, 1 deletion(-) diff --git a/magefile.go b/magefile.go index 5ed0d1d52..e149153b2 100644 --- a/magefile.go +++ b/magefile.go @@ -122,7 +122,6 @@ func UpdateTestfiles() { // Run smoke tests to quickly check if Porter is broken func TestSmoke() error { mg.Deps(tests.StartDockerRegistry) - defer tests.StopDockerRegistry() // Only do verbose output of tests when called with `mage -v TestSmoke` v := "" From 4b0bc757c14af643a059dc4b47716488a85f43f5 Mon Sep 17 00:00:00 2001 From: Carolyn Van Slyck Date: Tue, 15 Jun 2021 13:48:11 -0500 Subject: [PATCH 05/11] Fix branch name detection Don't assume the first branch name will work. Loop through matching refs, excluding tags, and look first for main and then for release/v*. Signed-off-by: Carolyn Van Slyck --- mage/git.go | 58 +++++++++++++++++++++++-------- mage/git_test.go | 89 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 14 deletions(-) create mode 100644 mage/git_test.go diff --git a/mage/git.go b/mage/git.go index e92644c2c..ba30be9bf 100644 --- a/mage/git.go +++ b/mage/git.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "log" "os" + "sort" "strings" "sync" @@ -78,24 +79,53 @@ func getVersion() string { return "v0.0.0" } -// Get the name of the current branch, or the branch that contains the current tag +// Return either "main", "v*", or "dev" for all other branches. func getBranchName() string { - // pull request - if branch, ok := os.LookupEnv("SYSTEM_PULLREQUEST_SOURCEBRANCH"); ok { - return branch + gitOutput, _ := must.OutputS("git", "for-each-ref", "--contains", "HEAD", "--format=%(refname)") + refs := strings.Split(gitOutput, "\n") + + return pickBranchName(refs) +} + +// Return either "main", "v*", or "dev" for all other branches. +func pickBranchName(refs []string) string { + var branch string + + if b, ok := os.LookupEnv("SYSTEM_PULLREQUEST_SOURCEBRANCH"); ok { + // pull request + branch = b + } else if b, ok := os.LookupEnv("BUILD_SOURCEBRANCHNAME"); ok { + // branch build + branch = b + } else { + // tag build + // Detect if this was a tag on main or a release + sort.Strings(refs) // put main ahead of release/v* + for _, ref := range refs { + // Ignore tags + if strings.HasSuffix(ref, "refs/tags") { + continue + } + + // Only match main and release/v* branches + if strings.HasSuffix(ref, "/main") || strings.Contains(ref, "/release/v") { + branch = ref + break + } + } } - // branch build - if branch, ok := os.LookupEnv("BUILD_SOURCEBRANCHNAME"); ok { - return branch + // Convert the ref name into a branch name, e.g. refs/heads/main -> main + branch = strings.NewReplacer("refs/heads/", "", "refs/remotes/origin/", "").Replace(branch) + + // Only use the following branch names "main", "release/v*", and "dev" for everything else + if branch != "main" && !strings.HasPrefix(branch, "release/v") { + branch = "dev" } - // tag build - // Use the first branch that contains the current commit - matches, _ := must.OutputS("git", "for-each-ref", "--contains", "HEAD", "--format=%(refname:short)") - firstMatchingBranch := strings.Split(matches, "\n")[0] - // The matching branch may be a remote branch, just get its name - return strings.Replace(firstMatchingBranch, "origin/", "", 1) + // Convert release/v1 -> v1 + branch = strings.ReplaceAll(branch, "release/", "") + return branch } func getPermalink() (string, bool) { @@ -111,7 +141,7 @@ func getPermalink() (string, bool) { // Get the current branch name, or the name of the branch we tagged from branch := getBranchName() - // Build a permalink such as "canary", "latest", "latest-v1", or "canary-v1" + // Build a permalink such as "canary", "latest", "latest-v1", or "dev-canary" switch branch { case "main": return permalinkPrefix, taggedRelease diff --git a/mage/git_test.go b/mage/git_test.go new file mode 100644 index 000000000..7fbf2edd1 --- /dev/null +++ b/mage/git_test.go @@ -0,0 +1,89 @@ +package mage + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPickBranchName(t *testing.T) { + // These aren't set locally but are set on a CI run + os.Unsetenv("SYSTEM_PULLREQUEST_SOURCEBRANCH") + os.Unsetenv("BUILD_SOURCEBRANCHNAME") + + t.Run("origin/main", func(t *testing.T) { + refs := []string{ + "refs/heads/foo", + "refs/remotes/origin/main", + "refs/remotes/origin/8252b6e4b1983702c7387ece7f971ef74047b746", + "refs/tags/v0.38.3", + } + branch := pickBranchName(refs) + assert.Equal(t, "main", branch) + }) + + t.Run("main", func(t *testing.T) { + refs := []string{ + "refs/heads/foo", + "refs/heads/main", + "refs/remotes/origin/8252b6e4b1983702c7387ece7f971ef74047b746", + "refs/tags/v0.38.3", + } + branch := pickBranchName(refs) + assert.Equal(t, "main", branch) + }) + + t.Run("pull request", func(t *testing.T) { + os.Setenv("SYSTEM_PULLREQUEST_SOURCEBRANCH", "patch-1") + defer os.Unsetenv("SYSTEM_PULLREQUEST_SOURCEBRANCH") + + refs := []string{ + "refs/remotes/origin/foo", + "refs/remotes/origin/8252b6e4b1983702c7387ece7f971ef74047b746", + } + branch := pickBranchName(refs) + assert.Equal(t, "dev", branch) + }) + + t.Run("branch build", func(t *testing.T) { + os.Setenv("BUILD_SOURCEBRANCHNAME", "foo") + defer os.Unsetenv("BUILD_SOURCEBRANCHNAME") + + refs := []string{ + "refs/remotes/origin/foo", + "refs/remotes/origin/8252b6e4b1983702c7387ece7f971ef74047b746", + } + branch := pickBranchName(refs) + assert.Equal(t, "dev", branch) + }) + + t.Run("tagged release on v1", func(t *testing.T) { + refs := []string{ + "refs/heads/release/v1", + "refs/remotes/origin/8252b6e4b1983702c7387ece7f971ef74047b746", + "refs/tags/v1.0.0-alpha.1", + } + branch := pickBranchName(refs) + assert.Equal(t, "v1", branch) + }) + + t.Run("tagged release on main", func(t *testing.T) { + refs := []string{ + "refs/heads/release/v1", + "refs/heads/main", + "refs/remotes/origin/8252b6e4b1983702c7387ece7f971ef74047b746", + "refs/tags/v0.38.3", + } + branch := pickBranchName(refs) + assert.Equal(t, "main", branch) + }) + + t.Run("local branch", func(t *testing.T) { + refs := []string{ + "refs/heads/foo", + } + branch := pickBranchName(refs) + assert.Equal(t, "dev", branch) + }) +} From 74d9609a6b79adc2ee0a2265528a6cdcb36e06d1 Mon Sep 17 00:00:00 2001 From: Carolyn Van Slyck Date: Tue, 15 Jun 2021 16:26:37 -0500 Subject: [PATCH 06/11] Rename publish-bundle to publish-examples Signed-off-by: Carolyn Van Slyck --- build/azure-pipelines.release-template.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/azure-pipelines.release-template.yml b/build/azure-pipelines.release-template.yml index fc8f41a77..9941d4a2f 100644 --- a/build/azure-pipelines.release-template.yml +++ b/build/azure-pipelines.release-template.yml @@ -223,5 +223,5 @@ stages: containerRegistryType: Container Registry dockerRegistryEndpoint: getporter-registry command: login - - script: REGISTRY=${{parameters.registry}} make publish-bundle + - script: REGISTRY=${{parameters.registry}} make publish-examples displayName: "Publish Example Bundles" From 42faaf8ccbd0a8b0c3b66fe2888f6e6a6328c4be Mon Sep 17 00:00:00 2001 From: Carolyn Van Slyck Date: Tue, 15 Jun 2021 17:14:07 -0500 Subject: [PATCH 07/11] Publish v1 releases to mixin feed The mixin feed should contain canary builds and tagged releases only. Signed-off-by: Carolyn Van Slyck --- mage/releases/publish.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mage/releases/publish.go b/mage/releases/publish.go index 2afedd4b3..30f0aa734 100644 --- a/mage/releases/publish.go +++ b/mage/releases/publish.go @@ -126,7 +126,7 @@ func PublishPlugin(plugin string) { func publishPackageFeed(pkgType string, name string) { info := mage.LoadMetadata() - if !info.ShouldPublishPermalink() { + if !(info.Permalink == "canary" || info.IsTaggedRelease) { fmt.Println("Skipping publish package feed for permalink", info.Permalink) return } From def8c5b068fd20a9b9b937cc52bddb05171d648d Mon Sep 17 00:00:00 2001 From: Carolyn Van Slyck Date: Wed, 16 Jun 2021 10:12:35 -0500 Subject: [PATCH 08/11] Publish dockerapp example to getporter Signed-off-by: Carolyn Van Slyck --- examples/dockerapp/porter.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/dockerapp/porter.yaml b/examples/dockerapp/porter.yaml index fe57e8867..6e0de585b 100644 --- a/examples/dockerapp/porter.yaml +++ b/examples/dockerapp/porter.yaml @@ -1,7 +1,7 @@ name: my-docker-app version: 0.1.0 description: My amazing docker app -registry: carolynvs +registry: getporter required: - docker: From 0fbb62d9faaeaf493e432359176a782c419cd7f0 Mon Sep 17 00:00:00 2001 From: Ritesh Yadav Date: Mon, 21 Jun 2021 19:57:32 +0530 Subject: [PATCH 09/11] Added suggested changes (#1640) Signed-off-by: Ritesh Yadav --- README.md | 31 +++++++++++++++++++ docs/static/images/mixins/docker_icon.png | Bin 0 -> 12479 bytes docs/static/images/mixins/exec.png | Bin 0 -> 10686 bytes docs/static/images/mixins/gcp.png | Bin 0 -> 30428 bytes docs/static/images/mixins/terraform_icon.png | Bin 0 -> 9106 bytes 5 files changed, 31 insertions(+) create mode 100644 docs/static/images/mixins/docker_icon.png create mode 100644 docs/static/images/mixins/exec.png create mode 100644 docs/static/images/mixins/gcp.png create mode 100644 docs/static/images/mixins/terraform_icon.png diff --git a/README.md b/README.md index 1cf91fa39..88ba6278f 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,37 @@ application.

Learn all about Porter at porter.sh

+# Porter Mixins + +Mixins provide out-of-the-box support for interacting with different tools and services from inside a bundle. You can always create a mixin, or use the exec mixin and a Custom Dockerfile if a custom mixin doesn't exist yet. + +[Porter Mixins](https://porter.sh/mixins/) are available for below platform's: + +| Platform | Supported? | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------: | +| [Docker](https://porter.sh/mixins/docker/) | ✔️ | +| [Docker-Compose](https://porter.sh/mixins/docker-compose/) | ✔️ | +| [Kubernetes](https://porter.sh/mixins/kubernetes/) | ✔️ | +| [Helm](https://porter.sh/mixins/helm/) | ✔️ | +| [GCloud](https://porter.sh/mixins/gcloud/) | ✔️ | +| [Terraform](https://porter.sh/mixins/terraform/) | ✔️ | +| [aws](https://porter.sh/mixins/aws/) | ✔️ | +| [Azure](https://porter.sh/mixins/azure/) | ✔️ | +| [exec](https://porter.sh/mixins/exec/) | ✔️ | + +# Porter Plugins + +Plugins let you store Porter's data and retrieve secrets from an external service. + +[Porter Plugins](https://porter.sh/plugins/) are available for below platform's: + +| Platform | Supported? | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------: | +| [Hashicorp](https://porter.sh/plugins/hashicorp/) | ✔️ | +| [Azure](https://porter.sh/plugins/azure/) | ✔️ | +| [Kubernetes](https://porter.sh/plugins/kubernetes/) | ✔️ | + + # Contact * [Mailing List] - Great for following the project at a high level because it is diff --git a/docs/static/images/mixins/docker_icon.png b/docs/static/images/mixins/docker_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..fd2d02e7a283759b4db96ed46d57f0fe9e67e6ce GIT binary patch literal 12479 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 P0*;!pmQt0%e_#F|?2eUX literal 0 HcmV?d00001 diff --git a/docs/static/images/mixins/gcp.png b/docs/static/images/mixins/gcp.png new file mode 100644 index 0000000000000000000000000000000000000000..d0f09d714436abf3008542c582466ae8cab57b75 GIT binary patch literal 30428 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

m2_B?e9-TUO5-BtiC5U=-S%VDoBv#dY$ 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 JxyJox{|At;Pk#Ua literal 0 HcmV?d00001 From 06ddc2f79bac201c44c5e00674bd29a3222df788 Mon Sep 17 00:00:00 2001 From: Carolyn Van Slyck Date: Tue, 29 Jun 2021 15:05:41 -0500 Subject: [PATCH 10/11] Improve credentials documentation * Add quickstart for credentials * Clarify the credentials page based on questions we have received * Add a crede faq about how to export and use a credential set * Add an example bundle that demonstrates credential sets Signed-off-by: Carolyn Van Slyck --- docs/config.toml | 12 +- docs/content/credentials.md | 58 ++++-- docs/content/parameters.md | 4 + docs/content/quickstart/_index.md | 2 +- docs/content/quickstart/credentials.md | 183 ++++++++++++++++++ docs/content/quickstart/parameters.md | 1 + .../themes/porter/layouts/section/single.html | 12 +- examples/credentials-tutorial/.dockerignore | 4 + examples/credentials-tutorial/.gitignore | 2 + examples/credentials-tutorial/Dockerfile.tmpl | 21 ++ examples/credentials-tutorial/README.md | 5 + examples/credentials-tutorial/helpers.sh | 9 + examples/credentials-tutorial/porter.yaml | 37 ++++ 13 files changed, 327 insertions(+), 23 deletions(-) create mode 100644 docs/content/quickstart/credentials.md create mode 100644 examples/credentials-tutorial/.dockerignore create mode 100644 examples/credentials-tutorial/.gitignore create mode 100644 examples/credentials-tutorial/Dockerfile.tmpl create mode 100644 examples/credentials-tutorial/README.md create mode 100755 examples/credentials-tutorial/helpers.sh create mode 100644 examples/credentials-tutorial/porter.yaml diff --git a/docs/config.toml b/docs/config.toml index 9f496f3f6..c8b6b2c48 100644 --- a/docs/config.toml +++ b/docs/config.toml @@ -38,22 +38,28 @@ pygmentsStyle = "friendly" url = "#" [[menu.main]] name = "Install Porter" - url = "/install" + url = "/install/" identifier = "install" weight = 11 parent = "get-started" [[menu.main]] name = "QuickStart" - url = "/quickstart" + url = "/quickstart/" identifier = "quickstart" weight = 30 parent = "get-started" [[menu.main]] name = "QuickStart - Parameters" - url = "/quickstart/parameters" + url = "/quickstart/parameters/" identifier = "quickstart-parameters" weight = 31 parent = "get-started" + [[menu.main]] + name = "QuickStart - Credentials" + url = "/quickstart/credentials/" + identifier = "quickstart-credentials" + weight = 32 + parent = "get-started" [[menu.main]] name = "Examples" url = "/examples/" diff --git a/docs/content/credentials.md b/docs/content/credentials.md index fb861c6f8..d52743343 100644 --- a/docs/content/credentials.md +++ b/docs/content/credentials.md @@ -8,13 +8,31 @@ aliases: When you are authoring a bundle, you can define what credentials your bundle requires such as a github token, cloud provider username/password, etc. Then in your action's steps you can reference the credentials using porter's template -language `{{ bundle.credentials.github_token }}`. +language `{{ bundle.credentials.github_token }}`, or directly access the +environment variable or path where the credential is stored. + +In the example below, the bundle defines two credentials. A kubeconfig file, +which once passed to the bundle is stored at /root/.kube/config, and a GitHub +token which once passed to the bundle is stored in the GITHUB_TOKEN environment +variable. + +```yaml +credentials: +- name: kubeconfig + path: /root/.kube/config +- name: token + env: GITHUB_TOKEN +``` + +The paths and environment variable names used in the credential +declaration represent where the value of the injected credentials are stored +_in the bundle when it is executing_. They are not used to locate the credential, +that is the responsibility of credential sets. ## Credential Sets -Credentials are injected when a bundle is executed -(install/upgrade/uninstall/invoke). First a user creates a credentials set using -[porter credentials generate][generate]. This is a mapping that tells porter +Before running a bundle the user must first create a credential set using +[porter credentials generate][generate]. A credentials set is a mapping that tells porter "given a name of a credential such as `github_token`, where can the value be found?". Credential values can be resolved from many places, such as environment variables or local files, or if you are using a [secrets @@ -30,15 +48,15 @@ to validate that you have created it properly. ## Runtime Now when you execute the bundle you can pass the credential set to the command -use `--cred` or `-c` flag, e.g. `porter install --cred github`. Before the -bundle is executed, porter users the credential set's mappings to retrieve the -credential values and then inject them into the bundle's execution environment, -e.g. the docker container, as environment variables. +with `--cred` or `-c` flags. For example, `porter install --cred github`. Before the +bundle is executed, Porter users the credential set's mappings to retrieve the +credential values, and then injects them into the bundle's execution environment +as either environment variables or files. -Inside the bundle's execution environment Porter looks for those environment -variables that represent the credentials and replaces the template placeholders +Inside the bundle's execution environment Porter replaces the template placeholders like `{{ bundle.credentials.github_token }}` with the actual credential value -before executing the step. +before executing the step. Credentials are also available directly through the +environment variable or path used in its declaration. Once the bundle finishes executing, the credentials are NOT recorded in the installation history. Parameters are recorded there so that you can view them @@ -46,6 +64,18 @@ later using `porter installations show NAME --output json`. ## Q & A +### Can I pass credentials to a bundle without credential sets? + +No, credentials must be passed to a bundle using credential sets. +Credentials are sensitive values and should ideally be sourced from a secret store such as Hashicorp Vault or Azure Key Vault to limit their exposure. + +If circumstances prevent you from using credential sets stored by Porter, you can export a credential set to a file and pass the file to a bundle as demonstrated below. + +``` +porter credentials show NAME --output json > creds.json +porter install --cred ./creds.json +``` + ### Why can't the credential source be defined in porter.yaml? The source of a credential is specific to each installation of the bundle. An @@ -59,4 +89,8 @@ after the production environment, or in a file under /tmp, or in their team’s key vault. This is why the author of the bundle can’t guess and put it in porter.yaml up front. -[generate]: /cli/porter_credentials_generate/ \ No newline at end of file +[generate]: /cli/porter_credentials_generate/ + +## Related + +* [QuickStart: Pass credentials to a bundle](/quickstart/credentials/) diff --git a/docs/content/parameters.md b/docs/content/parameters.md index 0526a28b6..2933f1c14 100644 --- a/docs/content/parameters.md +++ b/docs/content/parameters.md @@ -66,3 +66,7 @@ applies to parameter sources as well. [generate]: /cli/porter_parameters_generate/ [edit]: /cli/porter_parameters_edit/ + +## Related + +* [QuickStart: Use parameters with a bundle](/quickstart/parameters/) diff --git a/docs/content/quickstart/_index.md b/docs/content/quickstart/_index.md index 6837b8d76..2829e3dc3 100644 --- a/docs/content/quickstart/_index.md +++ b/docs/content/quickstart/_index.md @@ -158,5 +158,5 @@ porter uninstall porter-hello In this QuickStart, you learned how to use some of the features of the porter CLI to explain a bundle, install and manage its lifecycle. -* [Use parameters with a bundle](/quickstart/parameters/) +* [QuickStart: Use parameters with a bundle](/quickstart/parameters/) * [Learn more about use cases for bundles](/learning/#the-devil-is-in-the-deployments-bundle-use-cases) diff --git a/docs/content/quickstart/credentials.md b/docs/content/quickstart/credentials.md new file mode 100644 index 000000000..b3d228d1e --- /dev/null +++ b/docs/content/quickstart/credentials.md @@ -0,0 +1,183 @@ +--- +title: "QuickStart: Credentials" +descriptions: Learn how to use a bundle with credentials +layout: single +--- + +Now that you know how to customize a bundle installation with parameters, let's look at how your bundle can authenticate with **credentials**. +Credentials are sensitive values associated with your _identity_ and they are treated different from parameters by Porter. +All parameters, sensitive or otherwise, are stored in the installation history as a record of the values used when a bundle was run. +Credentials are never stored by Porter because it isn't safe to assume that identifying information contained in the credentials can be reused. + +Examples of a credential would be: a GitHub Personal Access Token, your cloud provider credentials, or a Kubernetes kubeconfig file. +We classify those values as credentials so that when different people execute that bundle, they provide their own personal credentials and execute the bundle with their user permissions. +In contrast, a database connection string used by your application is considered only to be a sensitive parameter because regardless of who is installing the bundle, the same connection string should be used. + +**If you want to use different values depending on the person executing the bundle, use credentials. Otherwise use sensitive parameters.** + +This is a convention recommended by Porter to avoid a situation where Sally installs a bundle with her personal credentials, and then every time another user subsequently upgrades the bundle, her credentials are re-used, making it look like Sally ran the upgrades. +Ultimately the difference between the parameters and credentisl is that credentials are never stored or reused by a bundle. + +Credentials are injected into a bundle as either an environment variable or a file. +Depending on the bundle, a credential can apply to all actions (install/upgrade/uninstall) or may only apply to a particular action. + +Let's look at a bundle with credentials: + +```console +$ porter explain --reference getporter/credentials-tutorial:v0.1.0 +Name: credentials-tutorial +Description: An example Porter bundle with credentials. Uses your GitHub token to retrieve your public user profile from GitHub. +Version: 0.1.0 +Porter Version: v0.38.1 + +Credentials: +Name Description Required Applies To +github-token A GitHub Personal Access Token. Generate one at https://github.com/settings/tokens. No scopes are required. true install,upgrade + +No parameters defined + +No outputs defined + +No custom actions defined + +No dependencies defined +``` + +In the Credentials section of the output returned by explain, there is a single required credential, github-token, that applies to the install and upgrade actions. +This means that the github-token credential is required to run porter install or porter upgrade, but is not required for porter uninstall. + +## Create a Credential Set + +Create a credential set for the credentials-tutorial bundle with the `porter credentials generate` command. It is an interactive command that walks through setting values for every credential in the specified bundle. + +```console +$ porter parameters generate github --reference getporter/credentials-tutorial:v0.1.0 +Generating new credential github from bundle credentials-tutorial +==> 1 credentials required for bundle credentials-tutorial +? How would you like to set credential "github-token" + [Use arrows to move, space to select, type to filter] + secret + specific value +> environment variable + file path + shell command +? Enter the environment variable that will be used to set credential "github-token" + GITHUB_TOKEN +``` + +This creates a credential set named github. +View the parameter set with the `porter parameters show` command: + +```console +$ porter credentials show github +Name: github +Created: 21 minutes ago +Modified: 21 minutes ago + +------------------------------------------- + Name Local Source Source Type +------------------------------------------- + github-token GITHUB_TOKEN env +``` + +The output shows that the credential set has one credential defined: github-token. The credential's value is not stored in the credential set, instead it only stores a mapping from the credential name to a location where the credential can be resolved, in this case an environment variable named GITHUB_TOKEN. + +In production it is a best practice to source sensitive values, either parameters or credentials, from a secret store, such as Hashicorp Vault or Azure Key Vault. +Avoid storing sensitive values in files or environment variables on developer and CI machines which could be compromised. +See the list of available [plugins](/plugins/) for which secret providers are supported. + +## Specify a credential with a Credential Set + +Pass credentials to a bundle with the \--cred or -c flag, where the flag value is either the name of a credential set stored in Porter, or a path to a credential set file. +For example: + +``` +porter install --cred github +``` + +The output of this example bundle prints data from your public GitHub user profile. + +```plaintext +executing install action from credentials-tutorial (installation: credentials-tutorial) +Retrieve current user profile from GitHub +{ + "login": "carolynvs", + "id": 1368985, + "node_id": "MDQ6VXNlcjEzNjg5ODU=", + "avatar_url": "https://avatars.githubusercontent.com/u/1368985?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/carolynvs", + "html_url": "https://github.com/carolynvs", + "followers_url": "https://api.github.com/users/carolynvs/followers", + "following_url": "https://api.github.com/users/carolynvs/following{/other_user}", + "gists_url": "https://api.github.com/users/carolynvs/gists{/gist_id}", + "starred_url": "https://api.github.com/users/carolynvs/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/carolynvs/subscriptions", + "organizations_url": "https://api.github.com/users/carolynvs/orgs", + "repos_url": "https://api.github.com/users/carolynvs/repos", + "events_url": "https://api.github.com/users/carolynvs/events{/privacy}", + "received_events_url": "https://api.github.com/users/carolynvs/received_events", + "type": "User", + "site_admin": false, + "name": "Carolyn Van Slyck", + "company": "@Azure ", + "blog": "carolynvanslyck.com", + "location": "Chicago, IL", + "email": null, + "hireable": null, + "bio": "Professional Yak Shaver", + "twitter_username": "carolynvs", + "public_repos": 244, + "public_gists": 26, + "followers": 297, + "following": 1, + "created_at": "2012-01-22T21:34:25Z", + "updated_at": "2021-06-28T14:32:27Z" +} +``` + +Sometimes it is easier to store the credential set in a file instead of having Porter configured with a storage plugin, for example on a test server. +In that case, you can save the credential set to a file with the following command: + +``` +porter credentials show github --output json > github-creds.json +``` + +The contents of the file are shown below: + +```json +{ + "schemaVersion": "1.0.0-DRAFT+b6c701f", + "name": "github", + "created": "2021-06-29T09:44:32.16657-05:00", + "modified": "2021-06-29T09:44:32.16657-05:00", + "credentials": [ + { + "name": "github-token", + "source": { + "env": "GITHUB_TOKEN" + } + } + ] +} +``` + +Below is an example of specifying the credential set with a filepath: + +``` +porter install --cred ./github-creds.json +``` + +## Cleanup + +To clean up the resources installed from this QuickStart, use the `porter uninstall` command. + +``` +porter uninstall credentials-tutorial +``` + +## Next Steps + +In this QuickStart, you learned how to see the credentials defined on a bundle, generate a credential set telling Porter where to find the credentials values, and pass credentials when executing a bundle. + +* [Understanding how credentials are resolved](/credentials/) diff --git a/docs/content/quickstart/parameters.md b/docs/content/quickstart/parameters.md index bec0ec65c..64f32dbe8 100644 --- a/docs/content/quickstart/parameters.md +++ b/docs/content/quickstart/parameters.md @@ -155,4 +155,5 @@ porter uninstall hello-llama In this QuickStart, you learned how to see the parameters defined on a bundle, their default values, and customize the installation of a bundle by specifying alternate values. +* [QuickStart: Pass credentials to a bundle](/quickstart/credentials/) * [Understanding how parameters are resolved](/parameters) diff --git a/docs/themes/porter/layouts/section/single.html b/docs/themes/porter/layouts/section/single.html index b0cd14b03..1edfe9706 100755 --- a/docs/themes/porter/layouts/section/single.html +++ b/docs/themes/porter/layouts/section/single.html @@ -3,19 +3,17 @@

{{ partial "docs-sidebar.html" . }} -
+
{{ partial "topbar.html" . }} -
-
 
-
+
+
 
+

{{ .Title }}

- {{ .Content }}
-
 
+
 
-
{{ partial "footer.html" . }} diff --git a/examples/credentials-tutorial/.dockerignore b/examples/credentials-tutorial/.dockerignore new file mode 100644 index 000000000..2919244c8 --- /dev/null +++ b/examples/credentials-tutorial/.dockerignore @@ -0,0 +1,4 @@ +# See https://docs.docker.com/engine/reference/builder/#dockerignore-file +# Put files here that you don't want copied into your bundle's invocation image +.gitignore +Dockerfile.tmpl diff --git a/examples/credentials-tutorial/.gitignore b/examples/credentials-tutorial/.gitignore new file mode 100644 index 000000000..35e26477e --- /dev/null +++ b/examples/credentials-tutorial/.gitignore @@ -0,0 +1,2 @@ +Dockerfile +.cnab/ diff --git a/examples/credentials-tutorial/Dockerfile.tmpl b/examples/credentials-tutorial/Dockerfile.tmpl new file mode 100644 index 000000000..81a0adcd0 --- /dev/null +++ b/examples/credentials-tutorial/Dockerfile.tmpl @@ -0,0 +1,21 @@ +FROM debian:stretch-slim + +ARG BUNDLE_DIR + +RUN apt-get update && apt-get install -y ca-certificates curl + +# This is a template Dockerfile for the bundle's invocation image +# You can customize it to use different base images, install tools and copy configuration files. +# +# Porter will use it as a template and append lines to it for the mixins +# and to set the CMD appropriately for the CNAB specification. +# +# Add the following line to porter.yaml to instruct Porter to use this template +# dockerfile: Dockerfile.tmpl + +# You can control where the mixin's Dockerfile lines are inserted into this file by moving "# PORTER_MIXINS" line +# another location in this file. If you remove that line, the mixins generated content is appended to this file. +# PORTER_MIXINS + +# Use the BUNDLE_DIR build argument to copy files into the bundle +COPY . $BUNDLE_DIR diff --git a/examples/credentials-tutorial/README.md b/examples/credentials-tutorial/README.md new file mode 100644 index 000000000..dc19a225a --- /dev/null +++ b/examples/credentials-tutorial/README.md @@ -0,0 +1,5 @@ +# Credentials Tutorial Bundle + +This bundle demonstrates how to define and use a credential in a bundle and is used in the [Credentials QuickStart]. + +[Credentials QuickStart]: https://porter.sh/quickstart/credentials/ diff --git a/examples/credentials-tutorial/helpers.sh b/examples/credentials-tutorial/helpers.sh new file mode 100755 index 000000000..ee273d8b8 --- /dev/null +++ b/examples/credentials-tutorial/helpers.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail + +getUser() { + curl -s -H "Accept: application/vnd.github.v3+json" -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/user +} + +# Call the requested function and pass the arguments as-is +"$@" diff --git a/examples/credentials-tutorial/porter.yaml b/examples/credentials-tutorial/porter.yaml new file mode 100644 index 000000000..4221da924 --- /dev/null +++ b/examples/credentials-tutorial/porter.yaml @@ -0,0 +1,37 @@ +name: credentials-tutorial +version: 0.1.0 +description: "An example Porter bundle with credentials. Uses your GitHub token to retrieve your public user profile from GitHub." +registry: getporter +dockerfile: Dockerfile.tmpl + +mixins: + - exec + +credentials: + - name: github-token + description: A GitHub Personal Access Token. Generate one at https://github.com/settings/tokens. No scopes are required. + env: GITHUB_TOKEN + applyTo: + - install + - upgrade + +install: + - exec: + description: "Retrieve current user profile from GitHub" + command: ./helpers.sh + arguments: + - getUser + +upgrade: + - exec: + description: "Retrieve current user profile from GitHub" + command: ./helpers.sh + arguments: + - getUser + +uninstall: + - exec: + description: "Uninstall credentials tutorial" + command: echo + arguments: + - "Nothing to uninstall. Bye!" From 53762822eda77d05162e66673c3c4e881518be99 Mon Sep 17 00:00:00 2001 From: Carolyn Van Slyck Date: Wed, 30 Jun 2021 12:09:13 -0500 Subject: [PATCH 11/11] Document helm3 mixin (#1652) * Document helm3 mixin * Rename the existing helm mixin page to /mixins/helm2 and indicate it is only for Helm v2. * Add a /mixins/helm3/ page for mchorfa's mixin which is the recommended mixin. * Redirect /mixins/helm/ to /mixins/helm3/ Signed-off-by: Carolyn Van Slyck * Fix typos Signed-off-by: Carolyn Van Slyck --- docs/content/mixins/{helm.md => helm2.md} | 10 +- docs/content/mixins/helm3.md | 172 ++++++++++++++++++++++ 2 files changed, 178 insertions(+), 4 deletions(-) rename docs/content/mixins/{helm.md => helm2.md} (92%) create mode 100644 docs/content/mixins/helm3.md diff --git a/docs/content/mixins/helm.md b/docs/content/mixins/helm2.md similarity index 92% rename from docs/content/mixins/helm.md rename to docs/content/mixins/helm2.md index 184aa8f24..ba35f0f62 100644 --- a/docs/content/mixins/helm.md +++ b/docs/content/mixins/helm2.md @@ -1,17 +1,19 @@ --- -title: helm mixin -description: Manage a Helm release with the helm CLI +title: helm v2 mixin +description: Manage a Helm release with the helm v2 CLI --- -This is a [Helm](https://helm.sh) mixin for -[Porter](https://github.com/getporter/porter). It executes the appropriate helm +This is a [Helm](https://helm.sh) v2 mixin for +[Porter](https://github.com/getporter/porter). It executes the appropriate helm v2 command based on which action it is included within: `install`, `upgrade`, or `delete`. Source: https://github.com/getporter/helm-mixin +⚠️ Helm v2 is no longer supported. Check out the [helm v3](/mixins/helm3/) mixin! + ### Install or Upgrade ```shell diff --git a/docs/content/mixins/helm3.md b/docs/content/mixins/helm3.md new file mode 100644 index 000000000..f0a8a0bd8 --- /dev/null +++ b/docs/content/mixins/helm3.md @@ -0,0 +1,172 @@ +--- +title: helm3 mixin +description: Manage a Helm release with the helm v3 CLI +aliases: +- /mixins/helm/ +--- + + + +This is a [Helm](https://helm.sh) v3 mixin for +[Porter](https://github.com/getporter/porter). It executes the appropriate helm v3 +command based on which action it is included within: `install`, `upgrade`, or +`delete`. + +Source: https://github.com/MChorfa/porter-helm3 + +### Install or Upgrade + +```shell +porter mixin install helm3 --feed-url https://mchorfa.github.io/porter-helm3/atom.xml +``` + +### Mixin Configuration + +Helm client version configuration. You can define others minors and patch versions up and down + +```yaml +- helm3: + clientVersion: v3.3.4 +``` + +Repositories + +```yaml +- helm3: + repositories: + stable: + url: "https://charts.helm.sh/stable" +``` + +### Mixin Syntax + +Install + +```yaml +install: + - helm3: + description: "Description of the command" + name: RELEASE_NAME + chart: STABLE_CHART_NAME + version: CHART_VERSION + namespace: NAMESPACE + replace: BOOL # Remove it if upsert is set to true. This is unsafe in production + devel: BOOL + wait: BOOL # default true + upsert: BOOL # default false. If set to true `upgrade --install` will be executed + set: + VAR1: VALUE1 + VAR2: VALUE2 +``` + +Upgrade + +```yaml +upgrade: + - helm3: + description: "Description of the command" + name: RELEASE_NAME + chart: STABLE_CHART_NAME + version: CHART_VERSION + namespace: NAMESPACE + resetValues: BOOL + reuseValues: BOOL + wait: BOOL # default true + set: + VAR1: VALUE1 + VAR2: VALUE2 +``` + +Uninstall + +```yaml +uninstall: + - helm3: + description: "Description of command" + namespace: NAMESPACE + releases: + - RELEASE_NAME1 + - RELASE_NAME2 +``` + +#### Outputs + +The mixin supports saving secrets from Kubernetes as outputs. + +```yaml +outputs: + - name: NAME + secret: SECRET_NAME + key: SECRET_KEY +``` + +The mixin also supports extracting resource metadata from Kubernetes as outputs. + +```yaml +outputs: + - name: NAME + resourceType: RESOURCE_TYPE + resourceName: RESOURCE_TYPE_NAME + namespace: NAMESPACE + jsonPath: JSON_PATH_DEFINITION +``` + +### Examples + +Install + +```yaml +install: + - helm3: + description: "Install MySQL" + name: mydb + chart: stable/mysql + version: 0.10.2 + namespace: mydb + replace: true + set: + mysqlDatabase: wordpress + mysqlUser: wordpress + outputs: + - name: mysql-root-password + secret: mydb-mysql + key: mysql-root-password + - name: mysql-password + secret: mydb-mysql + key: mysql-password + - name: mysql-cluster-ip + resourceType: service + resourceName: porter-ci-mysql-service + namespace: "default" + jsonPath: "{.spec.clusterIP}" +``` + +Upgrade + +```yaml +upgrade: + - helm3: + description: "Upgrade MySQL" + name: porter-ci-mysql + chart: stable/mysql + version: 0.10.2 + wait: true + resetValues: true + reuseValues: false + set: + mysqlDatabase: mydb + mysqlUser: myuser + livenessProbe.initialDelaySeconds: 30 + persistence.enabled: true +``` + +Uninstall + +```yaml +uninstall: + - helm3: + description: "Uninstall MySQL" + namespace: mydb + releases: + - mydb +```