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

feat: Remove binaries from argoexec image. Fixes #7486 #8292

Merged
merged 12 commits into from
Apr 5, 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
54 changes: 16 additions & 38 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
#syntax=docker/dockerfile:1.2

ARG DOCKER_CHANNEL=stable
ARG DOCKER_VERSION=20.10.14
# NOTE: kubectl version should be one minor version less than https://storage.googleapis.com/kubernetes-release/release/stable.txt
ARG KUBECTL_VERSION=1.22.3
ARG JQ_VERSION=1.6

FROM golang:1.17 as builder

RUN apt-get update && apt-get --no-install-recommends install -y \
Expand All @@ -15,9 +9,7 @@ RUN apt-get update && apt-get --no-install-recommends install -y \
apt-transport-https \
ca-certificates \
wget \
gcc \
libcap2-bin \
zip && \
gcc && \
apt-get clean \
&& rm -rf \
/var/lib/apt/lists/* \
Expand All @@ -37,33 +29,6 @@ RUN go mod download

COPY . .

####################################################################################################

FROM alpine:3 as argoexec-base

ARG DOCKER_CHANNEL
ARG DOCKER_VERSION
ARG KUBECTL_VERSION

RUN apk --no-cache add curl git tar jq

COPY hack/arch.sh hack/os.sh /bin/

RUN if [ $(arch.sh) = ppc64le ] || [ $(arch.sh) = s390x ]; then \
curl -o docker.tgz https://download.docker.com/$(os.sh)/static/${DOCKER_CHANNEL}/$(uname -m)/docker-18.06.3-ce.tgz; \
else \
curl -o docker.tgz https://download.docker.com/$(os.sh)/static/${DOCKER_CHANNEL}/$(uname -m)/docker-${DOCKER_VERSION}.tgz; \
fi && \
tar --extract --file docker.tgz --strip-components 1 --directory /usr/local/bin/ && \
rm docker.tgz
RUN curl -o /usr/local/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/v${KUBECTL_VERSION}/bin/$(os.sh)/$(arch.sh)/kubectl && \
chmod +x /usr/local/bin/kubectl
RUN rm /bin/arch.sh /bin/os.sh

COPY hack/ssh_known_hosts /etc/ssh/
COPY hack/nsswitch.conf /etc/


####################################################################################################

FROM node:16 as argo-ui
Expand All @@ -81,6 +46,15 @@ RUN NODE_OPTIONS="--max-old-space-size=2048" JOBS=max yarn --cwd ui build

FROM builder as argoexec-build

COPY hack/arch.sh hack/os.sh /bin/

# NOTE: kubectl version should be one minor version less than https://storage.googleapis.com/kubernetes-release/release/stable.txt
RUN curl -o /usr/local/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/v1.22.3/bin/$(os.sh)/$(arch.sh)/kubectl && \
chmod +x /usr/local/bin/kubectl

RUN curl -o /usr/local/bin/jq https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 && \
chmod +x /usr/local/bin/jq

# Tell git to forget about all of the files that were not included because of .dockerignore in order to ensure that
# the git state is "clean" even though said .dockerignore files are not present
RUN cat .dockerignore >> .gitignore
Expand Down Expand Up @@ -118,10 +92,14 @@ RUN --mount=type=cache,target=/root/.cache/go-build make dist/argo

####################################################################################################

FROM argoexec-base as argoexec
FROM gcr.io/distroless/static as argoexec

COPY --from=argoexec-build /go/src/github.com/argoproj/argo-workflows/dist/argoexec /usr/local/bin/
COPY --from=argoexec-build /usr/local/bin/kubectl /bin/
COPY --from=argoexec-build /usr/local/bin/jq /bin/
COPY --from=argoexec-build /go/src/github.com/argoproj/argo-workflows/dist/argoexec /bin/
COPY --from=argoexec-build /etc/mime.types /etc/mime.types
COPY hack/ssh_known_hosts /etc/ssh/
COPY hack/nsswitch.conf /etc/

ENTRYPOINT [ "argoexec" ]

Expand Down
165 changes: 53 additions & 112 deletions workflow/artifacts/git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@ import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"

"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
Expand Down Expand Up @@ -45,65 +42,33 @@ func GetUser(url string) string {
return "git"
}

func (g *ArtifactDriver) auth(sshUser string) (func(), transport.AuthMethod, []string, error) {
func (g *ArtifactDriver) auth(sshUser string) (func(), transport.AuthMethod, error) {
if g.SSHPrivateKey != "" {
signer, err := ssh.ParsePrivateKey([]byte(g.SSHPrivateKey))
if err != nil {
return nil, nil, nil, err
return nil, nil, err
}
privateKeyFile, err := ioutil.TempFile("", "id_rsa.")
if err != nil {
return nil, nil, nil, err
return nil, nil, err
}
err = ioutil.WriteFile(privateKeyFile.Name(), []byte(g.SSHPrivateKey), 0o600)
if err != nil {
return nil, nil, nil, err
return nil, nil, err
}
auth := &ssh2.PublicKeys{User: sshUser, Signer: signer}
if g.InsecureIgnoreHostKey {
auth.HostKeyCallback = ssh.InsecureIgnoreHostKey()
}
args := []string{"ssh", "-i", privateKeyFile.Name()}
alexec marked this conversation as resolved.
Show resolved Hide resolved
if g.InsecureIgnoreHostKey {
args = append(args, "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null")
} else {
args = append(args, "-o", "StrictHostKeyChecking=yes", "-o")
}
env := []string{"GIT_SSH_COMMAND=" + strings.Join(args, " ")}
if g.InsecureIgnoreHostKey {
auth.HostKeyCallback = ssh.InsecureIgnoreHostKey()
env = append(env, "GIT_SSL_NO_VERIFY=true")
}
return func() { _ = os.Remove(privateKeyFile.Name()) },
auth,
env,
nil
return func() { _ = os.Remove(privateKeyFile.Name()) }, auth, nil
}
if g.Username != "" || g.Password != "" {
filename := filepath.Join(os.TempDir(), "git-ask-pass.sh")
_, err := os.Stat(filename)
if os.IsNotExist(err) {
//nolint:gosec
err := ioutil.WriteFile(filename, []byte(`#!/bin/sh
case "$1" in
Username*) echo "${GIT_USERNAME}" ;;
Password*) echo "${GIT_PASSWORD}" ;;
esac
`), 0o755)
if err != nil {
return nil, nil, nil, err
}
}
return func() {},
&http.BasicAuth{Username: g.Username, Password: g.Password},
[]string{
"GIT_ASKPASS=" + filename,
"GIT_USERNAME=" + g.Username,
"GIT_PASSWORD=" + g.Password,
},
nil
return func() {}, &http.BasicAuth{Username: g.Username, Password: g.Password}, nil
}
return func() {}, nil, nil, nil
return func() {}, nil, nil
}

// Save is unsupported for git output artifacts
Expand All @@ -112,111 +77,87 @@ func (g *ArtifactDriver) Save(string, *wfv1.Artifact) error {
}

func (g *ArtifactDriver) Load(inputArtifact *wfv1.Artifact, path string) error {
sshUser := GetUser(inputArtifact.Git.Repo)
closer, auth, env, err := g.auth(sshUser)
a := inputArtifact.Git
sshUser := GetUser(a.Repo)
closer, auth, err := g.auth(sshUser)
if err != nil {
return err
}
defer closer()

var recurseSubmodules = git.DefaultSubmoduleRecursionDepth
if inputArtifact.Git.DisableSubmodules {
log.Info("Recursive cloning of submodules is disabled")
recurseSubmodules = git.NoRecurseSubmodules
}
repo, err := git.PlainClone(path, false, &git.CloneOptions{
URL: inputArtifact.Git.Repo,
RecurseSubmodules: recurseSubmodules,
Auth: auth,
Depth: inputArtifact.Git.GetDepth(),
})
depth := a.GetDepth()
r, err := git.PlainClone(path, false, &git.CloneOptions{URL: a.Repo, Auth: auth, Depth: depth})
switch err {
case transport.ErrEmptyRemoteRepository:
log.Info("Cloned an empty repository ")
log.Info("Cloned an empty repository")
r, err := git.PlainInit(path, false)
if err != nil {
return err
return fmt.Errorf("failed to plain init: %w", err)
}
if _, err := r.CreateRemote(&config.RemoteConfig{Name: git.DefaultRemoteName, URLs: []string{inputArtifact.Git.Repo}}); err != nil {
return err
if _, err := r.CreateRemote(&config.RemoteConfig{Name: git.DefaultRemoteName, URLs: []string{a.Repo}}); err != nil {
return fmt.Errorf("failed to create remote %q: %w", a.Repo, err)
}
branchName := inputArtifact.Git.Revision
branchName := a.Revision
if branchName == "" {
branchName = "master"
}
if err = r.CreateBranch(&config.Branch{Name: branchName, Remote: git.DefaultRemoteName, Merge: plumbing.Master}); err != nil {
return err
return fmt.Errorf("failed to create branch %q: %w", branchName, err)
}
return nil
default:
return err
case nil:
// fallthrough ...
default:
return fmt.Errorf("failed to clone %q: %w", a.Repo, err)
}
if inputArtifact.Git.Fetch != nil {
refSpecs := make([]config.RefSpec, len(inputArtifact.Git.Fetch))
for i, spec := range inputArtifact.Git.Fetch {
if len(a.Fetch) > 0 {
refSpecs := make([]config.RefSpec, len(a.Fetch))
for i, spec := range a.Fetch {
refSpecs[i] = config.RefSpec(spec)
}
fetchOptions := git.FetchOptions{
Auth: auth,
RefSpecs: refSpecs,
Depth: inputArtifact.Git.GetDepth(),
opts := &git.FetchOptions{Auth: auth, RefSpecs: refSpecs, Depth: depth}
if err := opts.Validate(); err != nil {
return fmt.Errorf("failed to validate fetch %v: %w", refSpecs, err)
}
if err = r.Fetch(opts); isFetchErr(err) {
return fmt.Errorf("failed to fetch %v: %w", refSpecs, err)
}
}
w, err := r.Worktree()
if err != nil {
return fmt.Errorf("failed to get work tree: %w", err)
}
if a.Revision != "" {
if err := r.Fetch(&git.FetchOptions{RefSpecs: []config.RefSpec{"refs/heads/*:refs/heads/*"}}); isFetchErr(err) {
alexec marked this conversation as resolved.
Show resolved Hide resolved
return fmt.Errorf("failed to fatch refs: %w", err)
}
err = fetchOptions.Validate()
h, err := r.ResolveRevision(plumbing.Revision(a.Revision))
if err != nil {
return err
return fmt.Errorf("failed to get resolve revision: %w", err)
}
err = repo.Fetch(&fetchOptions)
if isAlreadyUpToDateErr(err) {
return err
if err := w.Checkout(&git.CheckoutOptions{Hash: plumbing.NewHash(h.String())}); err != nil {
return fmt.Errorf("failed to checkout %q: %w", h, err)
}
}
if inputArtifact.Git.Revision != "" {
// We still rely on forking git for checkout, since go-git does not have a reliable
// way of resolving revisions (e.g. mybranch, HEAD^, v1.2.3)
rev := getRevisionForCheckout(inputArtifact.Git.Revision)
log.Info("Checking out revision ", rev)
cmd := exec.Command("git", "checkout", rev, "--")
cmd.Dir = path
cmd.Env = env
output, err := cmd.Output()
if !a.DisableSubmodules {
s, err := w.Submodules()
if err != nil {
return g.error(err, cmd)
}
log.Infof("`%s` stdout:\n%s", cmd.Args, string(output))
if !inputArtifact.Git.DisableSubmodules {
submodulesCmd := exec.Command("git", "submodule", "update", "--init", "--recursive", "--force")
submodulesCmd.Dir = path
submodulesCmd.Env = env
submoduleOutput, err := submodulesCmd.Output()
if err != nil {
return g.error(err, cmd)
}
log.Infof("`%s` stdout:\n%s", cmd.Args, string(submoduleOutput))
return fmt.Errorf("failed to get submodules: %w", err)
}
if err := s.Update(&git.SubmoduleUpdateOptions{
Init: true,
RecurseSubmodules: git.DefaultSubmoduleRecursionDepth,
Auth: auth,
}); err != nil {
return fmt.Errorf("failed to update submodules: %w", err)
}
}
return nil
}

// getRevisionForCheckout trims "refs/heads/" from the revision name (if present)
// so that `git checkout` will succeed.
func getRevisionForCheckout(revision string) string {
return strings.TrimPrefix(revision, "refs/heads/")
}

func isAlreadyUpToDateErr(err error) bool {
func isFetchErr(err error) bool {
return err != nil && err.Error() != "already up-to-date"
}

func (g *ArtifactDriver) error(err error, cmd *exec.Cmd) error {
if exErr, ok := err.(*exec.ExitError); ok {
log.Errorf("`%s` stderr:\n%s", cmd.Args, string(exErr.Stderr))
return errors.New(strings.Split(string(exErr.Stderr), "\n")[0])
}
return err
}

func (g *ArtifactDriver) ListObjects(artifact *wfv1.Artifact) ([]string, error) {
return nil, fmt.Errorf("ListObjects is currently not supported for this artifact type, but it will be in a future version")
}
Loading