Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFD 58: Package Distribution #10746

Merged
merged 8 commits into from
Jun 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 123 additions & 1 deletion .drone.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5092,6 +5092,128 @@ volumes:
- name: dockersock
temp: {}

---
################################################
# Generated using dronegen, do not edit by hand!
# Use 'make dronegen' to update.
# Generated at dronegen/misc.go:133
################################################

kind: pipeline
type: kubernetes
name: migrate-apt-new-repos
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be a part of the drone pipeline?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought about this a lot while building this part out. Putting it in the pipeline is by far the easiest way to implement migrations for older versions. In 99.9% of cases this pipeline will never be ran, but when we need to add previous versions this will save significant time. This section along with this new dronegen function takes another migration process like I did last week and turns it into a 5m change.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why this particular pipeline is here, though. I get publish-apt-new-repos, but not this one. Is it just marking that you intend to implement the migration of legacy here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pipeline is the implementation of old artifact migration. It's just not enabled/ran unless dronegen is configured to migrate specific versions. When specific versions are added to this function then running make dronegen will add migration steps to this pipeline for Drone to run. When there are not any functions listed there, running make dronegen will replace the pipeline with a "NOP" pipeline to prevent repeated migrations.

trigger:
event:
include:
- custom
repo:
include:
- non-existent-repository
branch:
include:
- non-existent-branch
clone:
disable: true
steps:
- name: Placeholder
image: alpine:latest
commands:
- echo "This command, step, and pipeline never runs"

---
################################################
# Generated using dronegen, do not edit by hand!
# Use 'make dronegen' to update.
# Generated at dronegen/misc.go:157
################################################

kind: pipeline
type: kubernetes
name: publish-apt-new-repos
trigger:
event:
include:
- promote
target:
include:
- production
repo:
include:
- gravitational/teleport
workspace:
path: /go
clone:
disable: true
steps:
- name: Verify build is tagged
image: alpine:latest
commands:
- '[ -n ${DRONE_TAG} ] || (echo ''DRONE_TAG is not set. Is the commit tagged?''
&& exit 1)'
- name: Check out code
image: alpine/git:latest
commands:
- mkdir -p /go/src/github.com/gravitational/teleport
- cd /go/src/github.com/gravitational/teleport
- git clone https://github.com/gravitational/${DRONE_REPO_NAME}.git .
- git checkout "${DRONE_TAG}"
- name: Download artifacts for "${DRONE_TAG}"
image: amazon/aws-cli
commands:
- mkdir -pv "$ARTIFACT_PATH"
- aws s3 sync --no-progress --delete --exclude "*" --include "*.deb*" s3://$AWS_S3_BUCKET/teleport/tag/${DRONE_TAG##v}/
"$ARTIFACT_PATH"
environment:
ARTIFACT_PATH: /go/artifacts
AWS_ACCESS_KEY_ID:
from_secret: AWS_ACCESS_KEY_ID
AWS_S3_BUCKET:
from_secret: AWS_S3_BUCKET
AWS_SECRET_ACCESS_KEY:
from_secret: AWS_SECRET_ACCESS_KEY
- name: Publish debs to APT repos for "${DRONE_TAG}"
image: golang:1.18.1-bullseye
commands:
- mkdir -pv -m0700 $GNUPGHOME
- echo "$GPG_RPM_SIGNING_ARCHIVE" | base64 -d | tar -xzf - -C $GNUPGHOME
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need post-build cleanup?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can certainly add rm -rf $GNUPGHOME, but I figured it was unnecessary as it's stored in a temp in-memory volume with no future steps

- chown -R root:root $GNUPGHOME
- apt update
- apt install aptly tree -y
- cd /go/src/github.com/gravitational/teleport/build.assets/tooling
- export VERSION="${DRONE_TAG}"
- export RELEASE_CHANNEL="stable"
- go run ./cmd/build-apt-repos -bucket "$APT_S3_BUCKET" -local-bucket-path "$BUCKET_CACHE_PATH"
-artifact-version "$VERSION" -release-channel "$RELEASE_CHANNEL" -aptly-root-dir
"$APTLY_ROOT_DIR" -artifact-path "$ARTIFACT_PATH" -log-level 4
- rm -rf "$BUCKET_CACHE_PATH"
- df -h "$APTLY_ROOT_DIR"
environment:
APT_S3_BUCKET:
from_secret: APT_REPO_NEW_AWS_S3_BUCKET
APTLY_ROOT_DIR: /mnt/aptly
ARTIFACT_PATH: /go/artifacts
AWS_ACCESS_KEY_ID:
from_secret: APT_REPO_NEW_AWS_ACCESS_KEY_ID
AWS_REGION: us-west-2
AWS_SECRET_ACCESS_KEY:
from_secret: APT_REPO_NEW_AWS_SECRET_ACCESS_KEY
BUCKET_CACHE_PATH: /tmp/bucket
GNUPGHOME: /tmpfs/gnupg
GPG_RPM_SIGNING_ARCHIVE:
from_secret: GPG_RPM_SIGNING_ARCHIVE
volumes:
- name: aptrepo
path: /mnt
- name: tmpfs
path: /tmpfs
volumes:
- name: aptrepo
claim:
name: drone-s3-aptrepo-pvc
- name: tmpfs
temp:
medium: memory

---
kind: pipeline
type: kubernetes
Expand Down Expand Up @@ -5480,6 +5602,6 @@ volumes:
name: drone-s3-debrepo-pvc
---
kind: signature
hmac: e83f39ac80fa38122a8cf34a6202f36a6d08163e8a31c30ce2e7599222f8b103
hmac: 0f665f13b0e591f35ceb28f503ac0e81e219011914af7d27bcb1ced907ebd397

...
215 changes: 215 additions & 0 deletions build.assets/tooling/cmd/build-apt-repos/apt_repo_tool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
/*
Copyright 2022 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 (
"io/fs"
"path/filepath"
"strings"
"time"

"github.com/gravitational/trace"
"github.com/sirupsen/logrus"
"golang.org/x/mod/semver"
)

type AptRepoTool struct {
config *Config
aptly *Aptly
s3Manager *S3manager
supportedOSs map[string][]string
}

// Instantiates a new apt repo tool instance and performs any required setup/config.
func NewAptRepoTool(config *Config, supportedOSs map[string][]string) (*AptRepoTool, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not make supportedOSs a part of the Config?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When writing this I thought that would be a great idea, but ultimately decided against it as it has several downsides:

  • The flags library does not support it, so implementing a map[string][]string input would likely add several hundred lines of parsing and code
  • Whatever format I went with would make reading the .drone.yml and related dronegen function more difficult, and would make adding/removing OS and OS versions more difficult, or would make the dronegen function more brittle

Not pulling in the supportedOSs map makes the code overall easier to read and maintain, and pushes the burden of validating the map to the compiler.

art := &AptRepoTool{
config: config,
s3Manager: NewS3Manager(config.bucketName),
supportedOSs: supportedOSs,
}

aptly, err := NewAptly(config.aptlyPath)
if err != nil {
return nil, trace.Wrap(err, "failed to create a new aptly instance")
}

art.aptly = aptly

return art, nil
}

// Runs the tool, creating and updating APT repos based upon the current configuration.
func (art *AptRepoTool) Run() error {
start := time.Now()
logrus.Infoln("Starting APT repo build process...")

isFirstRun, err := art.aptly.IsFirstRun()
if err != nil {
return trace.Wrap(err, "failed to check if Aptly needs (re)built")
}

if isFirstRun {
logrus.Warningln("First run or disaster recovery detected, attempting to rebuild existing repos from APT repository...")

err = art.s3Manager.DownloadExistingRepo(art.config.localBucketPath)
if err != nil {
return trace.Wrap(err, "failed to sync existing repo from S3 bucket")
}

_, err = art.recreateExistingRepos(art.config.localBucketPath)
if err != nil {
return trace.Wrap(err, "failed to recreate existing repos")
}
}

// Note: this logic will only push the artifact into the `art.supportedOSs` repos.
// This behavior is intended to allow deprecating old OS versions in the future
// without removing the associated repos entirely.
artifactRepos, err := art.getArtifactRepos()
if err != nil {
return trace.Wrap(err, "failed to create repos")
}

err = art.importNewDebs(artifactRepos)
if err != nil {
return trace.Wrap(err, "failed to import new debs")
}

err = art.publishRepos()
if err != nil {
return trace.Wrap(err, "failed to publish repos")
}

err = art.s3Manager.UploadBuiltRepo(filepath.Join(art.aptly.rootDir, "public"))
if err != nil {
return trace.Wrap(err, "failed to sync changes to S3 bucket")
}

logrus.Infof("APT repo build process completed in %s", time.Since(start).Round(time.Millisecond))
return nil
}

func (art *AptRepoTool) publishRepos() error {
// Pull in all Aptly repos, not just the latest ones to ensure they all get built into APT repos correctly
repos, err := art.aptly.GetAllRepos()
if err != nil {
return trace.Wrap(err, "failed to get all Aptly repos")
}

// Build a map keyed by os info with value of all repos that support the os in the key
// This will be used to structure the publish command
logrus.Debugf("Categorizing repos according to OS info: %v", RepoNames(repos))
categorizedRepos := make(map[string][]*Repo)
for _, r := range repos {
if osRepos, ok := categorizedRepos[r.OSInfo()]; ok {
categorizedRepos[r.OSInfo()] = append(osRepos, r)
} else {
categorizedRepos[r.OSInfo()] = []*Repo{r}
}
}
logrus.Debugf("Categorized repos: %v", categorizedRepos)

for osInfo, osRepoList := range categorizedRepos {
if len(osRepoList) < 1 {
continue
}

err := art.aptly.PublishRepos(osRepoList, osRepoList[0].os, osRepoList[0].osVersion)
if err != nil {
return trace.Wrap(err, "failed to publish for os %q", osInfo)
}
}

return nil
}

func (art *AptRepoTool) recreateExistingRepos(localPublishedPath string) ([]*Repo, error) {
logrus.Infoln("Recreating previously published repos...")
createdRepos, err := art.aptly.CreateReposFromPublishedPath(localPublishedPath)
if err != nil {
return nil, trace.Wrap(err, "failed to recreate existing repos")
}

for _, repo := range createdRepos {
err := art.aptly.ImportDebsFromExistingRepo(repo)
if err != nil {
return nil, trace.Wrap(err, "failed to import debs from existing repo %q", repo.Name())
}
}

logrus.Infof("Recreated and imported pre-existing artifacts for %d repos", len(createdRepos))
return createdRepos, nil
}

func (art *AptRepoTool) getArtifactRepos() ([]*Repo, error) {
logrus.Infoln("Creating or getting Aptly repos for artifact requirements...")

artifactRepos, err := art.aptly.CreateReposFromArtifactRequirements(art.supportedOSs,
art.config.releaseChannel, semver.Major(art.config.artifactVersion))
if err != nil {
return nil, trace.Wrap(err, "failed to create or get repos from artifact requirements")
}

logrus.Infof("Created or got %d artifact Aptly repos", len(artifactRepos))
return artifactRepos, nil
}

func (art *AptRepoTool) importNewDebs(repos []*Repo) error {
logrus.Debugf("Importing new debs into %d repos: %q", len(repos), strings.Join(RepoNames(repos), "\", \""))
err := filepath.WalkDir(art.config.artifactPath,
func(debPath string, d fs.DirEntry, err error) error {
return art.importNewDebsWalker(debPath, d, err, repos)
},
)
if err != nil {
return trace.Wrap(err, "failed to find and import debs")
}

return nil
}

// This should not be used outside of importNewDebs
func (art *AptRepoTool) importNewDebsWalker(debPath string, d fs.DirEntry, err error, repos []*Repo) error {
if err != nil {
return trace.Wrap(err, "failure while searching %s for debs", debPath)
}

if d.IsDir() {
return nil
}

fileName := d.Name()
if filepath.Ext(fileName) != ".deb" {
return nil
}

// Import new artifacts into all repos that match the artifact's requirements
for _, repo := range repos {
// Other checks could be added here to ensure that a given deb gets added to the correct repo
// such as name or parent directory, facilitating os-specific artifacts
if repo.majorVersion != semver.Major(art.config.artifactVersion) || repo.releaseChannel != art.config.releaseChannel {
continue
}

err = art.aptly.ImportDeb(repo.Name(), debPath)
if err != nil {
return trace.Wrap(err, "failed to import deb from %s", debPath)
}
}

return nil
}
Loading