Added multiarch build support for teleport-operator (#16688)

* Added multiarch build support for teleport oss, ent, and fips

* Exported image/imageTag types

* Resigned dronegen

* Removed remainder of testing changes

* Removed changes to submodules

* Reverted dockerfile-fips change

* FIxed docs wording

* Un-exported most constants

* Removed teleport.e makefile deb call

* Moved "sed | cut magic" to files

* Re-added `mkdir -pv /go/cache` to push.go

* Command deterministic order fix

* Added staging-only tag pipeline

* Moved PR to teleport operator to minimize potential issue impact

* Updated promote to pull and push without build

* Made cron triggers not affect canonical tags

* Added check for pre-existing tags on immutable CRs

* Added immutability check to manifests

* Updated staging ecr to only apply $TIMESTAMP tag on cron triggers

* Updated triggerinfo struct to use a triggerflag struct

* Fixed makefile after git mistake

* Makefile fix

* PR fixes

* Moved internal tools Go version to constant

* Separated container images gofile into multiple files

* Moved testing comment

* Added licenses

* Reorganized and added docs for container images

* Moved const to correct file

* Tag trigger logic test

* Testing specific fix

* Moved testing to v10.3.2

* Make semver dirs

* Refactored local registry name/socket

* Merged previous dockerfile changes

* Added TARGETOS TARGETARCH args

* Updatd tag to testing tag

* Promotion logic test

* Promotion fixes

* Testing specific fix

* Removed prerelease check for testing

* Added staging login commands to promote

* Fixed missing credentials on promotion pull

* Rerun tag test with new "full" semver

* Made staging builds only publish full semver

* Added semver logging command

* Empty commit to trigger Drone

* Promotion test

* Fixed preceeding v on promote pull

* Empty commit to trigger Drone

* Re-enabled verify not prerelease step on promote

* Cron trigger test

* Testing fix

* Testing fix 2

* Added sleep timer on docker buildx build

* Testing cleanup
This commit is contained in:
fheinecke 2022-10-18 21:31:22 -05:00 committed by GitHub
parent 5621f42bef
commit 633b9582e7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 4217 additions and 174 deletions

3008
.drone.yml

File diff suppressed because it is too large Load diff

View file

@ -16,7 +16,6 @@ VERSION=12.0.0-dev
DOCKER_IMAGE_QUAY ?= quay.io/gravitational/teleport
DOCKER_IMAGE_ECR ?= public.ecr.aws/gravitational/teleport
DOCKER_IMAGE_STAGING ?= 146628656107.dkr.ecr.us-west-2.amazonaws.com/gravitational/teleport
DOCKER_IMAGE_OPERATOR_STAGING ?= 146628656107.dkr.ecr.us-west-2.amazonaws.com/gravitational/teleport-operator
GOPATH ?= $(shell go env GOPATH)
@ -1035,23 +1034,6 @@ publish-ci: image-ci
fi
if [ -f e/Makefile ]; then $(MAKE) -C e publish-ci; fi
# Docker image build for Teleport Operator
.PHONY: image-operator-ci
image-operator-ci:
make -C operator docker-build IMG="$(DOCKER_IMAGE_OPERATOR_STAGING):$(VERSION)"
# DOCKER_CLI_EXPERIMENTAL=enabled is set to allow inspecting the manifest for present images.
# https://docs.docker.com/engine/reference/commandline/cli/#experimental-features
# The internal staging images use amazon ECR's immutable repository settings. This makes overwrites impossible currently.
# This can cause issues when drone tagging pipelines must be re-run due to failures.
# Currently the work around for this is to not attempt to push to the image when it already exists.
.PHONY: publish-operator-ci
publish-operator-ci: image-operator-ci
@if DOCKER_CLI_EXPERIMENTAL=enabled docker manifest inspect "$(DOCKER_IMAGE_OPERATOR_STAGING):$(VERSION)" >/dev/null 2>&1; then \
echo "$(DOCKER_IMAGE_OPERATOR_STAGING):$(VERSION) already exists. "; \
else \
docker push "$(DOCKER_IMAGE_OPERATOR_STAGING):$(VERSION)"; \
fi
.PHONY: print-version
print-version:

View file

@ -1,5 +1,7 @@
# Those variables are extracted from build.assets/Makefile so they can be imported
# by other Makefiles
# These values may need to be updated in `dronegen/container_image_products.go` if
# they change here
BUILDBOX_VERSION ?= teleport11
BUILDBOX=public.ecr.aws/gravitational/teleport-buildbox:$(BUILDBOX_VERSION)

View file

@ -33,6 +33,16 @@ const (
// ProductionRegistryQuay is the production image registry that hosts images on quay.io. Will be deprecated in the future.
// See RFD 73 - https://github.com/gravitational/teleport/blob/c18c09f5d562dd46a509154eab4295ad39decc3c/rfd/0073-public-image-registry.md
ProductionRegistryQuay = "quay.io"
// Go version used by internal tools
GoVersion = "1.18"
// The name of this service must match k8s.io/apimachinery/pkg/util/validation `IsDNS1123Subdomain`
// so that it is resolvable
// See https://github.com/drone-runners/drone-runner-kube/blob/master/engine/compiler/compiler.go#L398
// for details
LocalRegistryHostname string = "drone-docker-registry"
LocalRegistrySocket string = LocalRegistryHostname + ":5000"
)
var (
@ -105,12 +115,20 @@ func pushTriggerForBranch(branches ...string) trigger {
return t
}
func cronTrigger(cronJobNames []string) trigger {
return trigger{
Cron: triggerRef{Include: cronJobNames},
Repo: triggerRef{Include: []string{"gravitational/teleport"}},
}
}
func cloneRepoCommands(cloneDirectory, commit string) []string {
return []string{
fmt.Sprintf("mkdir -pv %q", cloneDirectory),
fmt.Sprintf("cd %q", cloneDirectory),
`git init && git remote add origin ${DRONE_REMOTE_URL}`,
`git fetch origin --tags`,
"git init",
"git remote add origin ${DRONE_REMOTE_URL}",
"git fetch origin --tags",
fmt.Sprintf("git checkout -qf %q", commit),
}
}
@ -215,6 +233,27 @@ func dockerService(v ...volumeRef) service {
}
}
// Starts a container registry service at `LocalRegistrySocket`
// This can be pushed/pulled to via `docker push/pull <LocalRegistrySocket>:5000/image:tag`
func dockerRegistryService() service {
return service{
Name: LocalRegistryHostname,
Image: "registry:2",
}
}
// dockerVolumes returns a slice of volumes
// It includes the Docker socket volume by default, plus any extra volumes passed in
func dockerVolumes(v ...volume) []volume {
return append(v, volumeDocker)
}
// dockerVolumeRefs returns a slice of volumeRefs
// It includes the Docker socket volumeRef as a default, plus any extra volumeRefs passed in
func dockerVolumeRefs(v ...volumeRef) []volumeRef {
return append(v, volumeRefDocker)
}
// releaseMakefileTarget gets the correct Makefile target for a given arch/fips/centos combo
func releaseMakefileTarget(b buildType) string {
makefileTarget := fmt.Sprintf("release-%s", b.arch)
@ -251,17 +290,23 @@ func waitForDockerStep() step {
}
}
func verifyValidPromoteRunSteps(checkoutPath, commit string, isParallelismEnabled bool) []step {
tagStep := verifyTaggedStep()
cloneStep := cloneRepoStep(checkoutPath, commit)
verifyStep := verifyNotPrereleaseStep(checkoutPath)
if isParallelismEnabled {
cloneStep.DependsOn = []string{tagStep.Name}
verifyStep.DependsOn = []string{cloneStep.Name}
// waitForDockerStep returns a step which checks that the Docker registry is ready
func waitForDockerRegistryStep() step {
return step{
Name: "Wait for docker registry",
Image: "alpine",
Commands: []string{
"apk add curl",
fmt.Sprintf(`timeout 30s /bin/sh -c 'while [ "$(curl -s -o /dev/null -w %%{http_code} http://%s/)" != "200" ]; do sleep 1; done'`, LocalRegistrySocket),
},
}
}
return []step{tagStep, cloneStep, verifyStep}
func verifyValidPromoteRunSteps() []step {
tagStep := verifyTaggedStep()
verifyStep := verifyNotPrereleaseStep()
return []step{tagStep, verifyStep}
}
func verifyTaggedStep() step {
@ -283,13 +328,20 @@ func cloneRepoStep(clonePath, commit string) step {
}
}
func verifyNotPrereleaseStep(checkoutPath string) step {
func verifyNotPrereleaseStep() step {
clonePath := "/tmp/repo"
commands := []string{
"apk add git",
}
commands = append(commands, cloneRepoCommands(clonePath, "${DRONE_TAG}")...)
commands = append(commands,
fmt.Sprintf("cd %q", path.Join(clonePath, "build.assets", "tooling")),
"go run ./cmd/check -tag ${DRONE_TAG} -check prerelease || (echo '---> This is a prerelease, not continuing promotion for ${DRONE_TAG}' && exit 78)",
)
return step{
Name: "Check if tag is prerelease",
Image: "golang:1.18-alpine",
Commands: []string{
fmt.Sprintf("cd %q", path.Join(checkoutPath, "build.assets", "tooling")),
"go run ./cmd/check -tag ${DRONE_TAG} -check prerelease || (echo '---> This is a prerelease, not continuing promotion for ${DRONE_TAG}' && exit 78)",
},
Name: "Check if tag is prerelease",
Image: fmt.Sprintf("golang:%s-alpine", GoVersion),
Commands: commands,
}
}

View file

@ -0,0 +1,241 @@
// Copyright 2021 Gravitational, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"fmt"
"path"
"regexp"
)
// Describes a Gravitational "product", where a "product" is a piece of software
// that we provide to our customers via container repositories.
type Product struct {
Name string
DockerfilePath string
WorkingDirectory string // Working directory to use for "docker build".
DockerfileTarget string // Optional. Defines a dockerfile target to stop at on build.
SupportedArchs []string // ISAs that the builder should produce
SetupSteps []step // Product-specific steps that must be ran before building an image.
DockerfileArgBuilder func(arch string) []string // Generator that returns "docker build --arg" strings
ImageBuilder func(repo *ContainerRepo, tag *ImageTag) *Image // Generator that returns an Image struct that defines what "docker build" should produce
GetRequiredStepNames func(arch string) []string // Generator that returns the name of the steps that "docker build" should wait for
}
func NewTeleportOperatorProduct(cloneDirectory string) *Product {
name := "teleport-operator"
return &Product{
Name: name,
DockerfilePath: path.Join(cloneDirectory, "operator", "Dockerfile"),
WorkingDirectory: cloneDirectory,
SupportedArchs: []string{"amd64", "arm", "arm64"},
ImageBuilder: func(repo *ContainerRepo, tag *ImageTag) *Image {
return &Image{
Repo: repo,
Name: name,
Tag: tag,
}
},
DockerfileArgBuilder: func(arch string) []string {
buildboxName := fmt.Sprintf("%s/gravitational/teleport-buildbox", ProductionRegistry)
compilerName := ""
switch arch {
case "x86_64", "amd64":
compilerName = "x86_64-linux-gnu-gcc"
case "i686", "i386":
compilerName = "i686-linux-gnu-gcc"
case "arm64", "aarch64":
buildboxName += "-arm"
compilerName = "aarch64-linux-gnu-gcc"
// We may want to add additional arm ISAs in the future to support devices without hardware FPUs
case "armhf":
case "arm":
buildboxName += "-arm"
compilerName = "arm-linux-gnueabihf-gcc"
}
buildboxName += ":teleport11"
return []string{
fmt.Sprintf("BUILDBOX=%s", buildboxName),
fmt.Sprintf("COMPILER_NAME=%s", compilerName),
}
},
}
}
func (p *Product) getBaseImage(arch string, version *ReleaseVersion) *Image {
return &Image{
Name: p.Name,
Tag: &ImageTag{
ShellBaseValue: version.GetFullSemver().GetSemverValue(),
DisplayBaseValue: version.MajorVersion,
Arch: arch,
},
}
}
func (p *Product) GetLocalRegistryImage(arch string, version *ReleaseVersion) *Image {
image := p.getBaseImage(arch, version)
image.Repo = NewLocalContainerRepo()
return image
}
func (p *Product) GetStagingRegistryImage(arch string, version *ReleaseVersion, stagingRepo *ContainerRepo) *Image {
image := p.getBaseImage(arch, version)
image.Repo = stagingRepo
return image
}
func (p *Product) buildSteps(version *ReleaseVersion, setupStepNames []string, flags *TriggerFlags) []step {
steps := make([]step, 0)
stagingRepo := GetStagingContainerRepo(flags.UseUniqueStagingTag)
productionRepos := GetProductionContainerRepos()
for _, setupStep := range p.SetupSteps {
setupStep.DependsOn = append(setupStep.DependsOn, setupStepNames...)
steps = append(steps, setupStep)
setupStepNames = append(setupStepNames, setupStep.Name)
}
archBuildStepDetails := make([]*buildStepOutput, 0, len(p.SupportedArchs))
for i, supportedArch := range p.SupportedArchs {
// Include steps for building images from scratch
if flags.ShouldBuildNewImages {
archBuildStep, archBuildStepDetail := p.createBuildStep(supportedArch, version, i)
archBuildStep.DependsOn = append(archBuildStep.DependsOn, setupStepNames...)
if p.GetRequiredStepNames != nil {
archBuildStep.DependsOn = append(archBuildStep.DependsOn, p.GetRequiredStepNames(supportedArch)...)
}
steps = append(steps, archBuildStep)
archBuildStepDetails = append(archBuildStepDetails, archBuildStepDetail)
} else {
stagingImage := p.GetStagingRegistryImage(supportedArch, version, stagingRepo)
pullStagingImageStep, locallyPushedImage := stagingRepo.pullPushStep(stagingImage, setupStepNames)
steps = append(steps, pullStagingImageStep)
// Generate build details that point to the pulled staging images
archBuildStepDetails = append(archBuildStepDetails, &buildStepOutput{
StepName: pullStagingImageStep.Name,
BuiltImage: locallyPushedImage,
Version: version,
Product: p,
})
}
}
for _, containerRepo := range getReposToPublishTo(productionRepos, stagingRepo, flags) {
steps = append(steps, containerRepo.buildSteps(archBuildStepDetails, flags)...)
}
return steps
}
func getReposToPublishTo(productionRepos []*ContainerRepo, stagingRepo *ContainerRepo, flags *TriggerFlags) []*ContainerRepo {
stagingRepos := []*ContainerRepo{stagingRepo}
if flags.ShouldAffectProductionImages {
if !flags.ShouldBuildNewImages {
// In this case the images will be pulled from staging and therefor should not be re-published
// to staging
return productionRepos
}
return append(stagingRepos, productionRepos...)
}
return stagingRepos
}
func (p *Product) GetBuildStepName(arch string, version *ReleaseVersion) string {
localImageName := p.GetLocalRegistryImage(arch, version)
return fmt.Sprintf("Build %s image %q", p.Name, localImageName.GetDisplayName())
}
func cleanBuilderName(builderName string) string {
var invalidBuildxCharExpression = regexp.MustCompile(`[^a-zA-Z0-9._-]+`)
return invalidBuildxCharExpression.ReplaceAllString(builderName, "-")
}
func (p *Product) createBuildStep(arch string, version *ReleaseVersion, delay int) (step, *buildStepOutput) {
localRegistryImage := p.GetLocalRegistryImage(arch, version)
builderName := cleanBuilderName(fmt.Sprintf("%s-builder", localRegistryImage.GetDisplayName()))
buildxConfigFileDir := path.Join("/tmp", builderName)
buildxConfigFilePath := path.Join(buildxConfigFileDir, "buildkitd.toml")
buildxCreateCommand := "docker buildx create"
buildxCreateCommand += fmt.Sprintf(" --driver %q", "docker-container")
// This is set so that buildx can reach the local registry
buildxCreateCommand += fmt.Sprintf(" --driver-opt %q", "network=host")
buildxCreateCommand += fmt.Sprintf(" --name %q", builderName)
buildxCreateCommand += fmt.Sprintf(" --config %q", buildxConfigFilePath)
buildCommand := "docker buildx build"
buildCommand += " --push"
buildCommand += fmt.Sprintf(" --builder %q", builderName)
if p.DockerfileTarget != "" {
buildCommand += fmt.Sprintf(" --target %q", p.DockerfileTarget)
}
buildCommand += fmt.Sprintf(" --platform %q", "linux/"+arch)
buildCommand += fmt.Sprintf(" --tag %s", localRegistryImage.GetShellName())
buildCommand += fmt.Sprintf(" --file %q", p.DockerfilePath)
if p.DockerfileArgBuilder != nil {
for _, buildArg := range p.DockerfileArgBuilder(arch) {
buildCommand += fmt.Sprintf(" --build-arg %q", buildArg)
}
}
buildCommand += " " + p.WorkingDirectory
delayTime := delay * 5
step := step{
Name: p.GetBuildStepName(arch, version),
Image: "docker",
Volumes: dockerVolumeRefs(),
Environment: map[string]value{
"DOCKER_BUILDKIT": {
raw: "1",
},
},
Commands: []string{
// Without a delay buildx can occasionally try to pull base images faster than container registries will allow,
// triggering a rate limit.
fmt.Sprintf("echo 'Sleeping %ds to avoid registry pull rate limits' && sleep %d", delayTime, delayTime),
"docker run --privileged --rm tonistiigi/binfmt --install all",
fmt.Sprintf("mkdir -pv %q && cd %q", p.WorkingDirectory, p.WorkingDirectory),
fmt.Sprintf("mkdir -pv %q", buildxConfigFileDir),
fmt.Sprintf("echo '[registry.%q]' > %q", LocalRegistrySocket, buildxConfigFilePath),
fmt.Sprintf("echo ' http = true' >> %q", buildxConfigFilePath),
buildxCreateCommand,
buildCommand,
fmt.Sprintf("docker buildx rm %q", builderName),
fmt.Sprintf("rm -rf %q", buildxConfigFileDir),
},
}
return step, &buildStepOutput{
StepName: step.Name,
BuiltImage: localRegistryImage,
Version: version,
Product: p,
}
}

View file

@ -0,0 +1,163 @@
// Copyright 2021 Gravitational, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"fmt"
"path"
)
// Describes a Drone trigger as it pertains to container image building.
type TriggerInfo struct {
Trigger trigger
Name string
Flags *TriggerFlags
SupportedVersions []*ReleaseVersion
SetupSteps []step
}
// This type is mainly used to make passing these vars around cleaner
type TriggerFlags struct {
ShouldAffectProductionImages bool
ShouldBuildNewImages bool
UseUniqueStagingTag bool
ShouldOnlyPublishFullSemver bool
}
func NewTagTrigger(branchMajorVersion string) *TriggerInfo {
tagTrigger := triggerTag
return &TriggerInfo{
Trigger: tagTrigger,
Name: "tag",
Flags: &TriggerFlags{
ShouldAffectProductionImages: false,
ShouldBuildNewImages: true,
UseUniqueStagingTag: false,
ShouldOnlyPublishFullSemver: true,
},
SupportedVersions: []*ReleaseVersion{
{
MajorVersion: branchMajorVersion,
ShellVersion: "$DRONE_TAG",
RelativeVersionName: "branch",
},
},
}
}
func NewPromoteTrigger(branchMajorVersion string) *TriggerInfo {
promoteTrigger := triggerPromote
promoteTrigger.Target.Include = append(promoteTrigger.Target.Include, "promote-docker")
return &TriggerInfo{
Trigger: promoteTrigger,
Name: "promote",
Flags: &TriggerFlags{
ShouldAffectProductionImages: true,
ShouldBuildNewImages: false,
UseUniqueStagingTag: false,
ShouldOnlyPublishFullSemver: false,
},
SupportedVersions: []*ReleaseVersion{
{
MajorVersion: branchMajorVersion,
ShellVersion: "$DRONE_TAG",
RelativeVersionName: "branch",
},
},
SetupSteps: verifyValidPromoteRunSteps(),
}
}
func NewCronTrigger(latestMajorVersions []string) *TriggerInfo {
if len(latestMajorVersions) == 0 {
return nil
}
majorVersionVarDirectory := "/go/vars/full-version"
supportedVersions := make([]*ReleaseVersion, 0, len(latestMajorVersions))
if len(latestMajorVersions) > 0 {
latestMajorVersion := latestMajorVersions[0]
supportedVersions = append(supportedVersions, &ReleaseVersion{
MajorVersion: latestMajorVersion,
ShellVersion: readCronShellVersionCommand(majorVersionVarDirectory, latestMajorVersion),
RelativeVersionName: "current-version",
SetupSteps: []step{getLatestSemverStep(latestMajorVersion, majorVersionVarDirectory)},
})
if len(latestMajorVersions) > 1 {
for i, majorVersion := range latestMajorVersions[1:] {
supportedVersions = append(supportedVersions, &ReleaseVersion{
MajorVersion: majorVersion,
ShellVersion: readCronShellVersionCommand(majorVersionVarDirectory, majorVersion),
RelativeVersionName: fmt.Sprintf("previous-version-%d", i+1),
SetupSteps: []step{getLatestSemverStep(majorVersion, majorVersionVarDirectory)},
})
}
}
}
return &TriggerInfo{
Trigger: cronTrigger([]string{"teleport-container-images-cron"}),
Name: "cron",
Flags: &TriggerFlags{
ShouldAffectProductionImages: true,
ShouldBuildNewImages: true,
UseUniqueStagingTag: true,
ShouldOnlyPublishFullSemver: false,
},
SupportedVersions: supportedVersions,
}
}
func getLatestSemverStep(majorVersion string, majorVersionVarDirectory string) step {
// We don't use "/go/src/github.com/gravitational/teleport" here as a later stage
// may need to clone a different version, and "/go" persists between steps
cloneDirectory := "/tmp/teleport"
majorVersionVarPath := path.Join(majorVersionVarDirectory, majorVersion)
return step{
Name: fmt.Sprintf("Find the latest available semver for %s", majorVersion),
Image: fmt.Sprintf("golang:%s", GoVersion),
Commands: append(
cloneRepoCommands(cloneDirectory, fmt.Sprintf("branch/%s", majorVersion)),
fmt.Sprintf("mkdir -pv %q", majorVersionVarDirectory),
fmt.Sprintf("cd %q", path.Join(cloneDirectory, "build.assets", "tooling", "cmd", "query-latest")),
fmt.Sprintf("go run . %q > %q", majorVersion, majorVersionVarPath),
fmt.Sprintf("echo Found full semver \"$(cat %q)\" for major version %q", majorVersionVarPath, majorVersion),
),
}
}
func readCronShellVersionCommand(majorVersionDirectory, majorVersion string) string {
return fmt.Sprintf("$(cat '%s')", path.Join(majorVersionDirectory, majorVersion))
}
// Drone triggers must all evaluate to "true" for a pipeline to be executed.
// As a result these pipelines are duplicated for each trigger.
// See https://docs.drone.io/pipeline/triggers/ for details.
func (ti *TriggerInfo) buildPipelines() []pipeline {
pipelines := make([]pipeline, 0, len(ti.SupportedVersions))
for _, teleportVersion := range ti.SupportedVersions {
pipeline := teleportVersion.buildVersionPipeline(ti.SetupSteps, ti.Flags)
pipeline.Name += "-" + ti.Name
pipeline.Trigger = ti.Trigger
pipelines = append(pipelines, pipeline)
}
return pipelines
}

View file

@ -0,0 +1,115 @@
// Copyright 2021 Gravitational, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"fmt"
"strings"
)
func buildContainerImagePipelines() []pipeline {
// *************************************************************
// ****** These need to be updated on each major release. ******
// ****** After updating, "make dronegen" must be reran. ******
// *************************************************************
latestMajorVersions := []string{"v11", "v10", "v9"}
branchMajorVersion := "v11"
triggers := []*TriggerInfo{
NewTagTrigger(branchMajorVersion),
NewPromoteTrigger(branchMajorVersion),
NewCronTrigger(latestMajorVersions),
}
if configureForPRTestingOnly {
triggers = append(triggers, NewTestTrigger(prBranch, branchMajorVersion))
}
pipelines := make([]pipeline, 0, len(triggers))
for _, trigger := range triggers {
pipelines = append(pipelines, trigger.buildPipelines()...)
}
return pipelines
}
// Describes a container image. Used for both local and remove images.
type Image struct {
Repo *ContainerRepo
Name string
Tag *ImageTag
}
func (i *Image) GetShellName() string {
repo := strings.TrimSuffix(i.Repo.RegistryDomain, "/")
if i.Repo.RegistryOrg != "" {
repo = fmt.Sprintf("%s/%s", repo, i.Repo.RegistryOrg)
}
return fmt.Sprintf("%s/%s:%s", repo, i.Name, i.Tag.GetShellValue())
}
func (i *Image) GetDisplayName() string {
return fmt.Sprintf("%s:%s", i.Name, i.Tag.GetDisplayValue())
}
// Contains information about the tag portion of an image.
type ImageTag struct {
ShellBaseValue string // Should evaluate in a shell context to the tag's value
DisplayBaseValue string // Should be set to a human-readable version of ShellTag
Arch string
IsImmutable bool
}
func NewLatestTag() *ImageTag {
return &ImageTag{
ShellBaseValue: "latest",
DisplayBaseValue: "latest",
}
}
func (it *ImageTag) AppendString(s string) {
it.ShellBaseValue += fmt.Sprintf("-%s", s)
it.DisplayBaseValue += fmt.Sprintf("-%s", s)
}
func (it *ImageTag) IsMultArch() bool {
return it.Arch != ""
}
func (it *ImageTag) GetShellValue() string {
return it.getValue(it.ShellBaseValue)
}
func (it *ImageTag) GetDisplayValue() string {
return it.getValue(it.DisplayBaseValue)
}
func (it *ImageTag) getValue(baseValue string) string {
if it.Arch == "" {
return baseValue
}
return fmt.Sprintf("%s-%s", baseValue, it.Arch)
}
// The `step` struct doesn't contain enough information to setup
// dependent steps so we add that via this struct
// This is used internally to pass information around
type buildStepOutput struct {
StepName string
BuiltImage *Image
Version *ReleaseVersion
Product *Product
}

View file

@ -0,0 +1,230 @@
// Copyright 2021 Gravitational, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"fmt"
"path"
"strconv"
"strings"
)
const (
varDirectory = "/go/var"
)
// Describes a Teleport/repo release version. All product releases are tied to Teleport's release cycle
// via this struct.
type ReleaseVersion struct {
MajorVersion string // This is the major version of a given build. `SearchVersion` should match this when evaluated.
ShellVersion string // This value will be evaluated by the shell in the context of a Drone step
RelativeVersionName string // The set of values for this should not change between major releases
SetupSteps []step // Version-specific steps that must be ran before executing build and push steps
}
func (rv *ReleaseVersion) buildVersionPipeline(triggerSetupSteps []step, flags *TriggerFlags) pipeline {
pipelineName := fmt.Sprintf("teleport-container-images-%s", rv.RelativeVersionName)
setupSteps, dependentStepNames := rv.getSetupStepInformation(triggerSetupSteps)
pipeline := newKubePipeline(pipelineName)
pipeline.Workspace = workspace{Path: "/go"}
pipeline.Services = []service{
dockerService(),
dockerRegistryService(),
}
pipeline.Volumes = dockerVolumes()
pipeline.Environment = map[string]value{
"DEBIAN_FRONTEND": {
raw: "noninteractive",
},
}
pipeline.Steps = append(setupSteps, rv.buildSteps(dependentStepNames, flags)...)
return pipeline
}
func (rv *ReleaseVersion) getSetupStepInformation(triggerSetupSteps []step) ([]step, []string) {
triggerSetupStepNames := make([]string, 0, len(triggerSetupSteps))
for _, triggerSetupStep := range triggerSetupSteps {
triggerSetupStepNames = append(triggerSetupStepNames, triggerSetupStep.Name)
}
nextStageSetupStepNames := triggerSetupStepNames
if len(rv.SetupSteps) > 0 {
versionSetupStepNames := make([]string, 0, len(rv.SetupSteps))
for _, versionSetupStep := range rv.SetupSteps {
versionSetupStep.DependsOn = append(versionSetupStep.DependsOn, triggerSetupStepNames...)
versionSetupStepNames = append(versionSetupStepNames, versionSetupStep.Name)
}
nextStageSetupStepNames = versionSetupStepNames
}
setupSteps := append(triggerSetupSteps, rv.SetupSteps...)
return setupSteps, nextStageSetupStepNames
}
func (rv *ReleaseVersion) buildSteps(setupStepNames []string, flags *TriggerFlags) []step {
clonedRepoPath := "/go/src/github.com/gravitational/teleport"
steps := make([]step, 0)
setupSteps := []step{
waitForDockerStep(),
waitForDockerRegistryStep(),
cloneRepoStep(clonedRepoPath, rv.ShellVersion),
rv.buildSplitSemverSteps(flags.ShouldOnlyPublishFullSemver),
}
for _, setupStep := range setupSteps {
setupStep.DependsOn = append(setupStep.DependsOn, setupStepNames...)
steps = append(steps, setupStep)
setupStepNames = append(setupStepNames, setupStep.Name)
}
for _, product := range rv.getProducts(clonedRepoPath) {
steps = append(steps, product.buildSteps(rv, setupStepNames, flags)...)
}
return steps
}
type Semver struct {
Name string // Human-readable name for the information contained in the semver, i.e. "major"
FilePath string // The path under the working dir where the information can be read from
FieldCount int // The number of significant version fields available in the semver i.e. "v11" -> 1
IsImmutable bool
IsFull bool
}
func (rv *ReleaseVersion) GetSemvers() []*Semver {
return []*Semver{
{
Name: "major",
FilePath: path.Join(varDirectory, "major-version"),
FieldCount: 1,
IsImmutable: false,
},
{
Name: "minor",
FilePath: path.Join(varDirectory, "minor-version"),
FieldCount: 2,
IsImmutable: false,
},
rv.GetFullSemver(),
}
}
func (rv *ReleaseVersion) GetFullSemver() *Semver {
return &Semver{
// For releases this is the "canonical" semver.
// For prereleases this is canonical + metadata.
// This is done to keep prereleases pushed to staging
// from overwriting release versions.
Name: "full",
FilePath: path.Join(varDirectory, "full-version"),
IsImmutable: true,
IsFull: true,
}
}
func (s *Semver) GetSemverValue() string {
return fmt.Sprintf("$(cat %q)", s.FilePath)
}
func (rv *ReleaseVersion) buildSplitSemverSteps(onlyBuildFullSemver bool) step {
semvers := rv.GetSemvers()
// Build the commands that generate the semvers
commands := make([]string, 0, len(semvers))
stepNameVersions := make([]string, 0, len(semvers))
for _, semver := range semvers {
if onlyBuildFullSemver && !semver.IsFull {
continue
}
commands = append(commands, fmt.Sprintf("mkdir -pv $(dirname %q)", semver.FilePath))
if semver.IsFull {
// Special case for full semver where only the "v" should be trimmed
commands = append(commands, fmt.Sprintf("echo %s | sed 's/v//' > %q", rv.ShellVersion, semver.FilePath))
} else {
// Trim the semver metadata and some digits
// Ex: semver.FieldCount = 3, cutFieldString = "1,2,3"
cutFieldStrings := make([]string, 0, semver.FieldCount)
for i := 1; i <= semver.FieldCount; i++ {
cutFieldStrings = append(cutFieldStrings, strconv.Itoa(i))
}
cutFieldString := strings.Join(cutFieldStrings, ",")
commands = append(commands, fmt.Sprintf("echo %s | sed 's/v//' | cut -d'.' -f %q > %q",
rv.ShellVersion, cutFieldString, semver.FilePath))
}
// For debugging
commands = append(commands, fmt.Sprintf("echo %s", semver.GetSemverValue()))
stepNameVersions = append(stepNameVersions, semver.Name)
}
// Build the formatted, human-readable step name
concatStepNameVersions := "Build"
for i, stepNameVersion := range stepNameVersions {
if i+1 < len(stepNameVersions) {
// If not the last version name
concatStepNameVersions = fmt.Sprintf("%s %s,", concatStepNameVersions, stepNameVersion)
} else {
if len(stepNameVersions) > 1 {
concatStepNameVersions = fmt.Sprintf("%s and", concatStepNameVersions)
}
concatStepNameVersions = fmt.Sprintf("%s %s semver", concatStepNameVersions, stepNameVersion)
if len(stepNameVersions) > 1 {
concatStepNameVersions = fmt.Sprintf("%ss", concatStepNameVersions)
}
}
}
return step{
Name: concatStepNameVersions,
Image: "alpine",
Commands: commands,
}
}
func (rv *ReleaseVersion) getProducts(clonedRepoPath string) []*Product {
teleportOperatorProduct := NewTeleportOperatorProduct(clonedRepoPath)
products := make([]*Product, 0, 1)
products = append(products, teleportOperatorProduct)
return products
}
func (rv *ReleaseVersion) getTagsForVersion(onlyBuildFullSemver bool) []*ImageTag {
semvers := rv.GetSemvers()
imageTags := make([]*ImageTag, 0, len(semvers))
for _, semver := range semvers {
if onlyBuildFullSemver && !semver.IsFull {
continue
}
imageTags = append(imageTags, &ImageTag{
ShellBaseValue: semver.GetSemverValue(),
DisplayBaseValue: semver.Name,
IsImmutable: semver.IsImmutable,
})
}
return imageTags
}

View file

@ -0,0 +1,320 @@
// Copyright 2021 Gravitational, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"fmt"
"sort"
"strings"
"golang.org/x/exp/maps"
)
// Describes a registry and repo that images are to be published to.
type ContainerRepo struct {
Name string
IsProductionRepo bool
IsImmutable bool
EnvironmentVars map[string]value
RegistryDomain string
RegistryOrg string
LoginCommands []string
TagBuilder func(baseTag *ImageTag) *ImageTag // Postprocessor for tags that append CR-specific suffixes
}
func NewEcrContainerRepo(accessKeyIDSecret, secretAccessKeySecret, domain string, isProduction, isImmutable, guaranteeUnique bool) *ContainerRepo {
nameSuffix := "staging"
ecrRegion := StagingEcrRegion
loginSubcommand := "ecr"
if isProduction {
nameSuffix = "production"
ecrRegion = PublicEcrRegion
loginSubcommand = "ecr-public"
}
registryOrg := ProductionRegistryOrg
if configureForPRTestingOnly {
accessKeyIDSecret = testingSecretPrefix + accessKeyIDSecret
secretAccessKeySecret = testingSecretPrefix + secretAccessKeySecret
registryOrg = testingECRRegistryOrg
if !isProduction {
domain = testingECRDomain
ecrRegion = testingECRRegion
}
}
loginCommands := []string{
"apk add --no-cache aws-cli",
fmt.Sprintf("aws %s get-login-password --region=%s | docker login -u=\"AWS\" --password-stdin %s", loginSubcommand, ecrRegion, domain),
}
if guaranteeUnique {
loginCommands = append(loginCommands, "TIMESTAMP=$(date -d @\"$DRONE_BUILD_CREATED\" '+%Y%m%d%H%M')")
}
return &ContainerRepo{
Name: fmt.Sprintf("ECR - %s", nameSuffix),
IsProductionRepo: isProduction,
IsImmutable: isImmutable,
EnvironmentVars: map[string]value{
"AWS_ACCESS_KEY_ID": {
fromSecret: accessKeyIDSecret,
},
"AWS_SECRET_ACCESS_KEY": {
fromSecret: secretAccessKeySecret,
},
},
RegistryDomain: domain,
RegistryOrg: registryOrg,
LoginCommands: loginCommands,
TagBuilder: func(tag *ImageTag) *ImageTag {
if guaranteeUnique {
tag.AppendString("$TIMESTAMP")
}
return tag
},
}
}
func NewQuayContainerRepo(dockerUsername, dockerPassword string) *ContainerRepo {
registryOrg := ProductionRegistryOrg
if configureForPRTestingOnly {
dockerUsername = testingSecretPrefix + dockerUsername
dockerPassword = testingSecretPrefix + dockerPassword
registryOrg = testingQuayRegistryOrg
}
return &ContainerRepo{
Name: "Quay",
IsProductionRepo: true,
IsImmutable: false,
EnvironmentVars: map[string]value{
"QUAY_USERNAME": {
fromSecret: dockerUsername,
},
"QUAY_PASSWORD": {
fromSecret: dockerPassword,
},
},
RegistryDomain: ProductionRegistryQuay,
RegistryOrg: registryOrg,
LoginCommands: []string{
fmt.Sprintf("docker login -u=\"$QUAY_USERNAME\" -p=\"$QUAY_PASSWORD\" %q", ProductionRegistryQuay),
},
}
}
func NewLocalContainerRepo() *ContainerRepo {
return &ContainerRepo{
Name: "Local Registry",
IsProductionRepo: false,
IsImmutable: false,
RegistryDomain: LocalRegistrySocket,
}
}
func GetLocalContainerRepo() *ContainerRepo {
return NewLocalContainerRepo()
}
func GetStagingContainerRepo(uniqueStagingTag bool) *ContainerRepo {
return NewEcrContainerRepo("STAGING_TELEPORT_DRONE_USER_ECR_KEY", "STAGING_TELEPORT_DRONE_USER_ECR_SECRET", StagingRegistry, false, true, uniqueStagingTag)
}
func GetProductionContainerRepos() []*ContainerRepo {
return []*ContainerRepo{
NewQuayContainerRepo("PRODUCTION_QUAYIO_DOCKER_USERNAME", "PRODUCTION_QUAYIO_DOCKER_PASSWORD"),
NewEcrContainerRepo("PRODUCTION_TELEPORT_DRONE_USER_ECR_KEY", "PRODUCTION_TELEPORT_DRONE_USER_ECR_SECRET", ProductionRegistry, true, false, false),
}
}
func (cr *ContainerRepo) buildSteps(buildStepDetails []*buildStepOutput, flags *TriggerFlags) []step {
if len(buildStepDetails) == 0 {
return nil
}
steps := make([]step, 0)
// Tag and push, collecting the names of the tag/push steps and the images pushed.
imageTags := cr.BuildImageTags(buildStepDetails[0].Version, flags)
pushedImages := make(map[*ImageTag][]*Image, len(imageTags))
pushStepNames := make([]string, 0, len(buildStepDetails))
for _, buildStepDetail := range buildStepDetails {
pushStep, pushedArchImages := cr.tagAndPushStep(buildStepDetail, imageTags)
pushStepNames = append(pushStepNames, pushStep.Name)
for _, imageTag := range imageTags {
pushedImages[imageTag] = append(pushedImages[imageTag], pushedArchImages[imageTag])
}
steps = append(steps, pushStep)
}
// Create and push a manifest for each tag, referencing multiple architectures in the manifest
for _, imageTag := range imageTags {
multiarchImageTag := *imageTag
multiarchImageTag.Arch = ""
manifestImage := buildStepDetails[0].Product.ImageBuilder(cr, &multiarchImageTag)
manifestStepName := cr.createAndPushManifestStep(manifestImage, pushStepNames, pushedImages[imageTag])
steps = append(steps, manifestStepName)
}
return steps
}
func (cr *ContainerRepo) logoutCommand() string {
return fmt.Sprintf("docker logout %q", cr.RegistryDomain)
}
func (cr *ContainerRepo) buildCommandsWithLogin(wrappedCommands []string) []string {
if cr.LoginCommands == nil || len(cr.LoginCommands) == 0 {
return wrappedCommands
}
commands := make([]string, 0)
commands = append(commands, cr.LoginCommands...)
commands = append(commands, wrappedCommands...)
commands = append(commands, cr.logoutCommand())
return commands
}
func (cr *ContainerRepo) BuildImageRepo() string {
return fmt.Sprintf("%s/%s/", cr.RegistryDomain, cr.RegistryOrg)
}
func (cr *ContainerRepo) BuildImageTags(version *ReleaseVersion, flags *TriggerFlags) []*ImageTag {
tags := version.getTagsForVersion(flags.ShouldOnlyPublishFullSemver)
if cr.TagBuilder != nil {
for i, tag := range tags {
tags[i] = cr.TagBuilder(tag)
}
}
return tags
}
// Pulls an image with authentication pushes it to the local repo.
// Does not generate additional tags.
// Returns an *Image struct describing the locally pushed image.
func (cr *ContainerRepo) pullPushStep(image *Image, dependencySteps []string) (step, *Image) {
localRepo := GetLocalContainerRepo()
localRepoImage := *image
localRepoImage.Repo = localRepo
commands := image.Repo.buildCommandsWithLogin([]string{fmt.Sprintf("docker pull %s", image.GetShellName())})
commands = append(commands,
fmt.Sprintf("docker tag %s %s", image.GetShellName(), localRepoImage.GetShellName()),
fmt.Sprintf("docker push %s", localRepoImage.GetShellName()),
)
return step{
Name: fmt.Sprintf("Pull %s and push it to %s", image.GetDisplayName(), localRepo.Name),
Image: "docker",
Volumes: dockerVolumeRefs(),
Environment: cr.EnvironmentVars,
Commands: commands,
DependsOn: dependencySteps,
}, &localRepoImage
}
func (cr *ContainerRepo) tagAndPushStep(buildStepDetails *buildStepOutput, imageTags []*ImageTag) (step, map[*ImageTag]*Image) {
archImageMap := make(map[*ImageTag]*Image, len(imageTags))
for _, imageTag := range imageTags {
archTag := *imageTag
archTag.Arch = buildStepDetails.BuiltImage.Tag.Arch
archImage := buildStepDetails.Product.ImageBuilder(cr, &archTag)
archImageMap[imageTag] = archImage
}
// This is tracked separately as maps in golang have a non-deterministic order when iterated over.
// As a result, .drone.yml will be updated every time `make dronegen` is ran regardless of if there
// is a change to the map or not
// The order/comparator does not matter here as long as it is deterministic between dronegen runs
archImageKeys := maps.Keys(archImageMap)
sort.SliceStable(archImageKeys, func(i, j int) bool { return archImageKeys[i].GetDisplayValue() < archImageKeys[j].GetDisplayValue() })
pullCommands := []string{
fmt.Sprintf("docker pull %s", buildStepDetails.BuiltImage.GetShellName()),
}
tagAndPushCommands := make([]string, 0)
for _, archImageKey := range archImageKeys {
archImage := archImageMap[archImageKey]
// Skip pushing images if the tag or container registry is immutable
tagAndPushCommands = append(tagAndPushCommands, buildImmutableSafeCommands(archImageKey.IsImmutable || cr.IsImmutable, archImage.GetShellName(), []string{
fmt.Sprintf("docker tag %s %s", buildStepDetails.BuiltImage.GetShellName(), archImage.GetShellName()),
fmt.Sprintf("docker push %s", archImage.GetShellName()),
})...)
}
tagAndPushCommands = cr.buildCommandsWithLogin(tagAndPushCommands)
commands := append(pullCommands, tagAndPushCommands...)
dependencySteps := []string{}
if buildStepDetails.StepName != "" {
dependencySteps = append(dependencySteps, buildStepDetails.StepName)
}
step := step{
Name: fmt.Sprintf("Tag and push image %q to %s", buildStepDetails.BuiltImage.GetDisplayName(), cr.Name),
Image: "docker",
Volumes: dockerVolumeRefs(),
Environment: cr.EnvironmentVars,
Commands: commands,
DependsOn: dependencySteps,
}
return step, archImageMap
}
func (cr *ContainerRepo) createAndPushManifestStep(manifestImage *Image, pushStepNames []string, pushedImages []*Image) step {
if len(pushStepNames) == 0 {
return step{}
}
manifestCommandArgs := make([]string, 0, len(pushedImages))
for _, pushedImage := range pushedImages {
manifestCommandArgs = append(manifestCommandArgs, fmt.Sprintf("--amend %s", pushedImage.GetShellName()))
}
// Skip pushing manifest if the tag or container registry is immutable
commands := buildImmutableSafeCommands(manifestImage.Tag.IsImmutable || cr.IsImmutable, manifestImage.GetShellName(), []string{
fmt.Sprintf("docker manifest create %s %s", manifestImage.GetShellName(), strings.Join(manifestCommandArgs, " ")),
fmt.Sprintf("docker manifest push %s", manifestImage.GetShellName()),
})
return step{
Name: fmt.Sprintf("Create manifest and push %q to %s", manifestImage.GetDisplayName(), cr.Name),
Image: "docker",
Volumes: dockerVolumeRefs(),
Environment: cr.EnvironmentVars,
Commands: cr.buildCommandsWithLogin(commands),
DependsOn: pushStepNames,
}
}
func buildImmutableSafeCommands(isImmutable bool, imageToCheck string, commandsToRun []string) []string {
if !isImmutable {
return commandsToRun
}
conditionalCommand := fmt.Sprintf("docker manifest inspect %s > /dev/null 2>&1", imageToCheck)
commandToRun := strings.Join(commandsToRun, " && ")
return []string{fmt.Sprintf("%s && echo 'Found existing image, skipping' || (%s)", conditionalCommand, commandToRun)}
}

View file

@ -0,0 +1,85 @@
// Copyright 2021 Gravitational, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
// This file contains variables and functions to make testing of the container image build process
// more simple and easier.
// To run one of these pipelines locally:
// # Drone requires certain variables to be set
// export DRONE_REMOTE_URL="https://github.com/gravitational/teleport"
// export DRONE_SOURCE_BRANCH="$(git branch --show-current)"
// # `drone exec` does not support `exec` or `kubernetes` pipelines
// sed -i '' 's/type\: kubernetes/type\: docker/' .drone.yml && sed -i '' 's/type\: exec/type\: docker/' .drone.yml
// # Drone has a bug where "workspace" is appended to "/drone/src". This fixes that by updating references
// sed -i '' 's~/go/~/drone/src/go/~g' .drone.yml
// # Pull the current branch instead of v11
// sed -i '' "s~git checkout -qf \"\$(cat '/go/vars/full-version/v11')\"~git checkout -qf \"${DRONE_SOURCE_BRANCH}\"~" .drone.yml
// # `drone exec` does not properly map the workspace path. This creates a volume to be shared between steps
// # at the correct path
// DOCKER_VOLUME_NAME="go"
// docker volume create "$DOCKER_VOLUME_NAME"
// drone exec --trusted --pipeline teleport-container-images-current-version-cron --clone=false --volume "${DOCKER_VOLUME_NAME}:/go"
// # Cleanup
// docker volume rm "$DOCKER_VOLUME_NAME"
// If you are working on a PR/testing changes to this file you should configure the following for Drone testing:
// 1. Publish the branch you're working on
// 2. Set `prBranch` to the name of the branch in (1)
// 3. Set `configureForPRTestingOnly` to true
// 4. Create a public and private ECR, Quay repos for "teleport", "teleport-ent", "teleport-operator", "teleport-lab"
// 5. Set `testingQuayRegistryOrg` and `testingECRRegistryOrg` to the org name(s) used in (4)
// 6. Set the `ECRTestingDomain` to the domain used for the private ECR repos
// 7. Create two separate IAM users, each with full access to either the public ECR repo OR the private ECR repo
// 8. Create a Quay "robot account" with write permissions for the created Quay repos
// 9. Set the Drone secrets for the secret names listed in "GetContainerRepos" to the credentials in (7, 8), prefixed by the value of `testingSecretPrefix`
//
// On each commit, after running `make dronegen``, run the following commands and resign the file:
// # Pull the current branch instead of v11 so the appropriate dockerfile gets loaded
// sed -i '' "s~git checkout -qf \"\$(cat '/go/vars/full-version/v11')\"~git checkout -qf \"${DRONE_SOURCE_BRANCH}\"~" .drone.yml
//
// When finishing up your PR check the following:
// * The testing secrets added to Drone have been removed
// * `configureForPRTestingOnly` has been set to false, and `make dronegen` has been reran afterwords
const (
configureForPRTestingOnly bool = false
testingSecretPrefix string = "TEST_"
testingQuayRegistryOrg string = "" //"fred_heinecke"
testingECRRegistryOrg string = "u8j2q1d9"
testingECRRegion string = "us-east-2"
prBranch string = "" //"fred/multiarch-teleport-container-images"
testingECRDomain string = "278576220453.dkr.ecr.us-east-2.amazonaws.com"
)
const (
ProductionRegistryOrg string = "gravitational"
PublicEcrRegion string = "us-east-1"
StagingEcrRegion string = "us-west-2"
)
func NewTestTrigger(triggerBranch, testMajorVersion string) *TriggerInfo {
// baseTrigger := NewTagTrigger(testMajorVersion)
// baseTrigger := NewPromoteTrigger(testMajorVersion)
baseTrigger := NewCronTrigger([]string{testMajorVersion})
baseTrigger.Name = "Test trigger on push"
baseTrigger.Trigger = trigger{
Repo: triggerRef{Include: []string{"gravitational/teleport"}},
Event: triggerRef{Include: []string{"push"}},
Branch: triggerRef{Include: []string{triggerBranch}},
}
return baseTrigger
}

View file

@ -32,10 +32,11 @@ func main() {
pipelines = append(pipelines, pushPipelines()...)
pipelines = append(pipelines, tagPipelines()...)
pipelines = append(pipelines, cronPipelines()...)
pipelines = append(pipelines, artifactMigrationPipeline()...)
pipelines = append(pipelines, buildOsRepoPipelines()...)
pipelines = append(pipelines, promoteBuildPipelines()...)
pipelines = append(pipelines, updateDocsPipeline())
pipelines = append(pipelines, buildboxPipeline())
pipelines = append(pipelines, buildContainerImagePipelines()...)
pipelines = append(pipelines, publishReleasePipeline())
if err := writePipelines(".drone.yml", pipelines); err != nil {

View file

@ -18,13 +18,3 @@ func updateDocsPipeline() pipeline {
// TODO: migrate
return pipeline{}
}
func verifyTaggedBuildStep() step {
return step{
Name: "Verify build is tagged",
Image: "alpine:latest",
Commands: []string{
"[ -n ${DRONE_TAG} ] || (echo 'DRONE_TAG is not set. Is the commit tagged?' && exit 1)",
},
}
}

View file

@ -20,6 +20,13 @@ import (
"strings"
)
func buildOsRepoPipelines() []pipeline {
pipelines := promoteBuildOsRepoPipelines()
pipelines = append(pipelines, artifactMigrationPipeline()...)
return pipelines
}
func promoteBuildOsRepoPipelines() []pipeline {
aptPipeline := promoteAptPipeline()
yumPipeline := promoteYumPipeline()
@ -183,13 +190,15 @@ func (optpb *OsPackageToolPipelineBuilder) buildPromoteOsPackagePipeline() pipel
pipelineName := fmt.Sprintf("publish-%s", optpb.pipelineNameSuffix)
checkoutPath := "/go/src/github.com/gravitational/teleport"
commitName := "${DRONE_TAG}"
checkoutStepName := "Check out code"
p := optpb.buildBaseOsPackagePipeline(pipelineName, checkoutStepName, checkoutPath, commitName)
p := optpb.buildBaseOsPackagePipeline(pipelineName, checkoutPath, commitName)
p.Trigger = triggerPromote
p.Trigger.Repo.Include = []string{"gravitational/teleport"}
setupSteps := verifyValidPromoteRunSteps(checkoutPath, commitName, true)
setupSteps := append(
verifyValidPromoteRunSteps(),
cloneRepoStep(checkoutPath, commitName),
)
setupStepNames := make([]string, 0, len(setupSteps))
for _, setupStep := range setupSteps {
@ -218,14 +227,13 @@ func (optpb *OsPackageToolPipelineBuilder) buildMigrateOsPackagePipeline(trigger
// DRONE_TAG is not available outside of promotion pipelines and will cause drone to fail with a
// "migrate-apt-new-repos: bad substitution" error if used here
commitName := "${DRONE_COMMIT}"
checkoutStepName := "Check out code"
// If migrations are not configured then don't run
if triggerBranch == "" || len(migrationVersions) == 0 {
return buildNeverTriggerPipeline(pipelineName)
}
p := optpb.buildBaseOsPackagePipeline(pipelineName, checkoutStepName, checkoutPath, commitName)
p := optpb.buildBaseOsPackagePipeline(pipelineName, checkoutPath, commitName)
p.Trigger = trigger{
Repo: triggerRef{Include: []string{"gravitational/teleport"}},
Event: triggerRef{Include: []string{"push"}},
@ -268,7 +276,7 @@ func buildNeverTriggerPipeline(pipelineName string) pipeline {
// Functions that use this method should add at least:
// * a Trigger
// * Steps for checkout
func (optpb *OsPackageToolPipelineBuilder) buildBaseOsPackagePipeline(pipelineName, checkoutStepName, checkoutPath, commit string) pipeline {
func (optpb *OsPackageToolPipelineBuilder) buildBaseOsPackagePipeline(pipelineName, checkoutPath, commit string) pipeline {
p := newKubePipeline(pipelineName)
p.Workspace = workspace{Path: "/go"}
p.Volumes = []volume{
@ -281,13 +289,7 @@ func (optpb *OsPackageToolPipelineBuilder) buildBaseOsPackagePipeline(pipelineNa
volumeTmpfs,
volumeAwsConfig,
}
p.Steps = []step{
{
Name: checkoutStepName,
Image: "alpine/git:latest",
Commands: toolCheckoutCommands(checkoutPath, commit),
},
}
p.Steps = []step{cloneRepoStep(checkoutPath, commit)}
setStepResourceLimits(p.Steps)
return p
@ -310,17 +312,6 @@ func setStepResourceLimits(steps []step) {
// }
}
// Note that tags are also valid here as a tag refers to a specific commit
func toolCheckoutCommands(checkoutPath, commit string) []string {
commands := []string{
fmt.Sprintf("mkdir -p %q", checkoutPath),
fmt.Sprintf("cd %q", checkoutPath),
`git clone https://github.com/gravitational/${DRONE_REPO_NAME}.git .`,
fmt.Sprintf("git checkout %q", commit),
}
return commands
}
func (optpb *OsPackageToolPipelineBuilder) getDroneTagVersionSteps(codePath string) []step {
return optpb.getVersionSteps(codePath, "${DRONE_TAG}", true)
}
@ -409,7 +400,7 @@ func (optpb *OsPackageToolPipelineBuilder) getVersionSteps(codePath, version str
assumeUploadRoleStep,
{
Name: fmt.Sprintf("Publish %ss to %s repos for %q", optpb.packageType, strings.ToUpper(optpb.packageManagerName), version),
Image: "golang:1.18.4-bullseye",
Image: fmt.Sprintf("golang:%s-bullseye", GoVersion),
Environment: optpb.environmentVars,
Commands: append(
toolSetupCommands,

View file

@ -39,7 +39,7 @@ func buildDockerPromotionPipelineECR() pipeline {
volumeAwsConfig,
}
dockerPipeline.Steps = append(dockerPipeline.Steps, verifyTaggedBuildStep())
dockerPipeline.Steps = append(dockerPipeline.Steps, verifyTaggedStep())
dockerPipeline.Steps = append(dockerPipeline.Steps, waitForDockerStep())
// Pull/Push Steps
@ -68,13 +68,11 @@ func buildDockerPromotionPipelineECR() pipeline {
fmt.Sprintf("docker pull %s/gravitational/teleport:$${VERSION}", StagingRegistry),
fmt.Sprintf("docker pull %s/gravitational/teleport-ent:$${VERSION}", StagingRegistry),
fmt.Sprintf("docker pull %s/gravitational/teleport-ent:$${VERSION}-fips", StagingRegistry),
fmt.Sprintf("docker pull %s/gravitational/teleport-operator:$${VERSION}", StagingRegistry),
// retag images to production naming
"echo \"---> Tagging images for $${VERSION}\"",
fmt.Sprintf("docker tag %s/gravitational/teleport:$${VERSION} %s/gravitational/teleport:$${VERSION}", StagingRegistry, ProductionRegistry),
fmt.Sprintf("docker tag %s/gravitational/teleport-ent:$${VERSION} %s/gravitational/teleport-ent:$${VERSION}", StagingRegistry, ProductionRegistry),
fmt.Sprintf("docker tag %s/gravitational/teleport-ent:$${VERSION}-fips %s/gravitational/teleport-ent:$${VERSION}-fips", StagingRegistry, ProductionRegistry),
fmt.Sprintf("docker tag %s/gravitational/teleport-operator:$${VERSION} %s/gravitational/teleport-operator:$${VERSION}", StagingRegistry, ProductionRegistry),
// authenticate with production credentials
"docker logout " + StagingRegistry,
"aws ecr-public get-login-password --region=us-east-1 | docker login -u=\"AWS\" --password-stdin " + ProductionRegistry,
@ -84,7 +82,6 @@ func buildDockerPromotionPipelineECR() pipeline {
fmt.Sprintf("docker push %s/gravitational/teleport:$${VERSION}", ProductionRegistry),
fmt.Sprintf("docker push %s/gravitational/teleport-ent:$${VERSION}", ProductionRegistry),
fmt.Sprintf("docker push %s/gravitational/teleport-ent:$${VERSION}-fips", ProductionRegistry),
fmt.Sprintf("docker push %s/gravitational/teleport-operator:$${VERSION}", ProductionRegistry),
},
})
@ -106,7 +103,7 @@ func buildDockerPromotionPipelineQuay() pipeline {
volumeAwsConfig,
}
dockerPipeline.Steps = append(dockerPipeline.Steps, verifyTaggedBuildStep())
dockerPipeline.Steps = append(dockerPipeline.Steps, verifyTaggedStep())
dockerPipeline.Steps = append(dockerPipeline.Steps, waitForDockerStep())
// Pull/Push Steps
@ -139,13 +136,11 @@ func buildDockerPromotionPipelineQuay() pipeline {
fmt.Sprintf("docker pull %s/gravitational/teleport:$${VERSION}", StagingRegistry),
fmt.Sprintf("docker pull %s/gravitational/teleport-ent:$${VERSION}", StagingRegistry),
fmt.Sprintf("docker pull %s/gravitational/teleport-ent:$${VERSION}-fips", StagingRegistry),
fmt.Sprintf("docker pull %s/gravitational/teleport-operator:$${VERSION}", StagingRegistry),
// retag images to production naming
"echo \"---> Tagging images for $${VERSION}\"",
fmt.Sprintf("docker tag %s/gravitational/teleport:$${VERSION} %s/gravitational/teleport:$${VERSION}", StagingRegistry, ProductionRegistryQuay),
fmt.Sprintf("docker tag %s/gravitational/teleport-ent:$${VERSION} %s/gravitational/teleport-ent:$${VERSION}", StagingRegistry, ProductionRegistryQuay),
fmt.Sprintf("docker tag %s/gravitational/teleport-ent:$${VERSION}-fips %s/gravitational/teleport-ent:$${VERSION}-fips", StagingRegistry, ProductionRegistryQuay),
fmt.Sprintf("docker tag %s/gravitational/teleport-operator:$${VERSION} %s/gravitational/teleport-operator:$${VERSION}", StagingRegistry, ProductionRegistryQuay),
// authenticate with production credentials
"docker logout " + StagingRegistry,
"docker login -u=\"$QUAY_USERNAME\" -p=\"$QUAY_PASSWORD\" " + ProductionRegistryQuay,
@ -154,7 +149,6 @@ func buildDockerPromotionPipelineQuay() pipeline {
fmt.Sprintf("docker push %s/gravitational/teleport:$${VERSION}", ProductionRegistryQuay),
fmt.Sprintf("docker push %s/gravitational/teleport-ent:$${VERSION}", ProductionRegistryQuay),
fmt.Sprintf("docker push %s/gravitational/teleport-ent:$${VERSION}-fips", ProductionRegistryQuay),
fmt.Sprintf("docker push %s/gravitational/teleport-operator:$${VERSION}", ProductionRegistryQuay),
},
})

View file

@ -18,6 +18,7 @@ import "fmt"
// pushCheckoutCommands builds a list of commands for Drone to check out a git commit on a push build
func pushCheckoutCommands(b buildType) []string {
cloneDirectory := "/go/src/github.com/gravitational/teleport"
var commands []string
if b.hasTeleportConnect() {
@ -25,12 +26,9 @@ func pushCheckoutCommands(b buildType) []string {
commands = append(commands, `mkdir -p /go/src/github.com/gravitational/webapps`)
}
commands = append(commands, cloneRepoCommands(cloneDirectory, "${DRONE_COMMIT_SHA}")...)
commands = append(commands,
`mkdir -p /go/src/github.com/gravitational/teleport /go/cache`,
`cd /go/src/github.com/gravitational/teleport`,
`git init && git remote add origin ${DRONE_REMOTE_URL}`,
`git fetch origin`,
`git checkout -qf ${DRONE_COMMIT_SHA}`,
// this is allowed to fail because pre-4.3 Teleport versions don't use the webassets submodule
`git submodule update --init webassets || true`,
`mkdir -m 0700 /root/.ssh && echo "$GITHUB_PRIVATE_KEY" > /root/.ssh/id_rsa && chmod 600 /root/.ssh/id_rsa`,
@ -39,6 +37,7 @@ func pushCheckoutCommands(b buildType) []string {
// do a recursive submodule checkout to get both webassets and webassets/e
// this is allowed to fail because pre-4.3 Teleport versions don't use the webassets submodule
`git submodule update --init --recursive webassets || true`,
`mkdir -pv /go/cache`,
)
if b.hasTeleportConnect() {

View file

@ -1,5 +1,6 @@
ARG BUILDBOX
FROM $BUILDBOX as builder
# BUILDPLATFORM is provided by Docker/buildx
FROM --platform=$BUILDPLATFORM $BUILDBOX as builder
WORKDIR /go/src/github.com/gravitational/teleport
@ -7,11 +8,12 @@ WORKDIR /go/src/github.com/gravitational/teleport
COPY go.mod go.mod
COPY go.sum go.sum
# we have to copy the API before `go mod download` because go.mod has a replace directive for it
# We have to copy the API before `go mod download` because go.mod has a replace directive for it
COPY api/ api/
# cache deps before building and copying source
# this way we don't need to re-download deps when the deps are the same
# Download and Cache dependencies before building and copying source
# This will prevent re-downloading the operator's dependencies if they have not changed as this
# `run` layer will be cached
RUN go mod download
COPY *.go ./
@ -22,10 +24,20 @@ COPY operator/sidecar/ operator/sidecar/
COPY operator/main.go operator/main.go
COPY operator/namespace.go operator/namespace.go
# Build
RUN GOOS=linux GOARCH=amd64 go build -a -o /go/bin/teleport-operator github.com/gravitational/teleport/operator
# Compiler package should use host-triplet-agnostic name (i.e. "x86-64-linux-gnu-gcc" instead of "gcc")
# in most cases, to avoid issues on systems with multiple versions of gcc (i.e. buildboxes)
# TARGETOS and TARGETARCH are provided by Docker/buildx, but must be explicitly listed here
ARG COMPILER_NAME TARGETOS TARGETARCH
FROM gcr.io/distroless/cc
# Build the program
# CGO is required for github.com/gravitational/teleport/lib/system
RUN echo "Targeting $TARGETOS/$TARGETARCH with CC=$COMPILER_NAME" && \
CGO_ENABLED=1 CC=$COMPILER_NAME GOOS=$TARGETOS GOARCH=$TARGETARCH \
go build -a -o /go/bin/teleport-operator github.com/gravitational/teleport/operator
# Create the image with the build operator on the $TARGETPLATFORM
# TARGETPLATFORM is provided by Docker/buildx
FROM --platform=$TARGETPLATFORM gcr.io/distroless/cc
WORKDIR /
COPY --from=builder /go/bin/teleport-operator .

View file

@ -20,6 +20,25 @@ SHELL = /usr/bin/env bash -o pipefail
# include BUILDBOX_VERSION, BUILDBOX and BUILDBOX_variant variables
include ../build.assets/images.mk
# Configure which compiler and buildbox to use
OS ?= $(shell go env GOOS)
ARCH ?= $(shell go env GOARCH)
ifeq ("$(OS)","linux")
ifeq ("$(ARCH)","amd64")
COMPILER ?= x86_64-linux-gnu-gcc
PLATFORM_BUILDBOX ?= $(BUILDBOX)
else ifeq ("$(ARCH)","386")
COMPILER ?= x86_64-linux-gnu-gcc
PLATFORM_BUILDBOX ?= $(BUILDBOX)
else ifeq ("$(ARCH)","arm")
COMPILER ?= arm-linux-gnueabihf-gcc
PLATFORM_BUILDBOX ?= $(BUILDBOX_ARM)
else ifeq ("$(ARCH)","arm64")
COMPILER ?= aarch64-linux-gnu-gcc
PLATFORM_BUILDBOX ?= $(BUILDBOX_ARM)
endif
endif
.PHONY: all
all: build
@ -103,7 +122,8 @@ run: manifests generate fmt vet ## Run a controller from your host.
.PHONY: docker-build
docker-build: ## Build docker image with the manager.
docker build --build-arg BUILDBOX=$(BUILDBOX) -t ${IMG} .. -f ./Dockerfile
docker buildx build --platform="$(OS)/$(ARCH)" --build-arg BUILDBOX=$(PLATFORM_BUILDBOX) \
--build-arg COMPILER_NAME=$(COMPILER) -t ${IMG} --load .. -f ./Dockerfile
.PHONY: docker-push
docker-push: ## Push docker image with the manager.