Skip to content

Commit

Permalink
Local source code feature (using bundle images)
Browse files Browse the repository at this point in the history
Reference shipwright-io/build#717

Add `bundle` package that contains convenience code to push a local
source code directory as a bundle image to a container registry.

Add new flags for container image and local directory settings.
  • Loading branch information
HeavyWombat committed Aug 10, 2021
1 parent b1b7e40 commit 20e1b46
Show file tree
Hide file tree
Showing 12 changed files with 391 additions and 50 deletions.
228 changes: 228 additions & 0 deletions pkg/shp/bundle/bundle.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
package bundle

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"

"k8s.io/cli-runtime/pkg/genericclioptions"

"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/schollz/progressbar/v3"
"github.com/shipwright-io/build/pkg/reconciler/buildrun/resources/sources"
)

// Push bundles the provided local directory into a container image and pushes
// it to the given registry.
func Push(ctx context.Context, io *genericclioptions.IOStreams, localDirectory string, targetImage string) (name.Digest, error) {
tag, err := name.NewTag(targetImage)
if err != nil {
return name.Digest{}, err
}

auth, err := authn.DefaultKeychain.Resolve(tag.Context())
if err != nil {
return name.Digest{}, err
}

updates := make(chan v1.Update, 1)
done := make(chan struct{}, 1)
go func() {
var progress *progressbar.ProgressBar
for {
select {
case <-ctx.Done():
return

case <-done:
return

case update, ok := <-updates:
if !ok {
return
}

if progress == nil {
progress = progressbar.NewOptions(int(update.Total),
progressbar.OptionSetWriter(io.ErrOut),
progressbar.OptionEnableColorCodes(true),
progressbar.OptionShowBytes(true),
progressbar.OptionSetWidth(15),
progressbar.OptionSetPredictTime(false),
progressbar.OptionSetDescription("Uploading local source..."),
progressbar.OptionSetTheme(progressbar.Theme{
Saucer: "[green]=[reset]",
SaucerHead: "[green]>[reset]",
SaucerPadding: " ",
BarStart: "[",
BarEnd: "]"}),
progressbar.OptionClearOnFinish(),
)
defer progress.Close()
}

progress.ChangeMax64(update.Total)
_ = progress.Set64(update.Complete)
}
}
}()

fmt.Fprintf(io.Out, "Bundling %q as %q ...\n", localDirectory, targetImage)
digest, err := sources.Bundle(
tag,
localDirectory,
remote.WithContext(ctx),
remote.WithAuth(auth),
remote.WithProgress(updates),
)

done <- struct{}{}
return digest, err
}

func Prune(ctx context.Context, io *genericclioptions.IOStreams, image string) error {
ref, err := name.ParseReference(image)
if err != nil {
return err
}

auth, err := authn.DefaultKeychain.Resolve(ref.Context())
if err != nil {
return err
}

switch ref.Context().RegistryStr() {
case "index.docker.io":
list, err := remote.ListWithContext(ctx, ref.Context(), remote.WithAuth(auth))
if err != nil {
return err
}

switch len(list) {
case 0:
return nil

case 1:
authr, err := auth.Authorization()
if err != nil {
return err
}

token, err := dockerHubLogin(authr.Username, authr.Password)
if err != nil {
return err
}

return dockerHubRepoDelete(token, ref)

default:
fmt.Fprintf(io.ErrOut, "removing specific tags is currently not supported for images hosted on %s\n", ref.Context().RegistryStr())

// In case the input argument included a digest, the reference
// needs to be updated to exclude the digest for the empty image
// override to succeed.
switch ref.(type) {
case name.Digest:
tmp := strings.SplitN(image, "@", 2)
ref, err = name.NewTag(tmp[0])
if err != nil {
return err
}
}

return remote.Write(ref, empty.Image, remote.WithContext(ctx), remote.WithAuth(auth))
}

default:
return remote.Delete(
ref,
remote.WithContext(ctx),
remote.WithAuth(auth),
)
}
}

func dockerHubLogin(username string, password string) (string, error) {
type LoginData struct {
Username string `json:"username"`
Password string `json:"password"`
}

loginData, err := json.Marshal(LoginData{Username: username, Password: password})
if err != nil {
return "", err
}

req, err := http.NewRequest("POST", "https://hub.docker.com/v2/users/login/", bytes.NewReader(loginData))
if err != nil {
return "", err
}

req.Header.Set("Content-Type", "application/json")

resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}

defer resp.Body.Close()

bodyData, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}

switch resp.StatusCode {
case http.StatusOK:
type LoginToken struct {
Token string `json:"token"`
}

var loginToken LoginToken
if err := json.Unmarshal(bodyData, &loginToken); err != nil {
return "", err
}

return loginToken.Token, nil

default:
return "", fmt.Errorf(string(bodyData))
}
}

func dockerHubRepoDelete(token string, ref name.Reference) error {
req, err := http.NewRequest("DELETE", fmt.Sprintf("https://hub.docker.com/v2/repositories/%s/", ref.Context().RepositoryStr()), nil)
if err != nil {
return err
}

req.Header.Set("Authorization", "JWT "+token)

resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}

defer resp.Body.Close()

respData, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}

switch resp.StatusCode {
case http.StatusAccepted:
return nil

default:
return fmt.Errorf("failed with HTTP status code %d: %s", resp.StatusCode, string(respData))
}
}
27 changes: 20 additions & 7 deletions pkg/shp/cmd/build/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ package build
import (
"fmt"

buildv1alpha1 "github.com/shipwright-io/build/pkg/apis/build/v1alpha1"
"github.com/shipwright-io/cli/pkg/shp/cmd/runner"
"github.com/shipwright-io/cli/pkg/shp/flags"
"github.com/shipwright-io/cli/pkg/shp/params"
"github.com/spf13/cobra"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/cli-runtime/pkg/genericclioptions"

buildv1alpha1 "github.com/shipwright-io/build/pkg/apis/build/v1alpha1"
"github.com/shipwright-io/cli/pkg/shp/cmd/runner"
"github.com/shipwright-io/cli/pkg/shp/flags"
"github.com/shipwright-io/cli/pkg/shp/params"
)

// CreateCommand contains data input from user
Expand Down Expand Up @@ -48,6 +49,19 @@ func (c *CreateCommand) Validate() error {
if c.name == "" {
return fmt.Errorf("name must be provided")
}

if c.buildSpec.Source.URL != "" &&
c.buildSpec.Source.Container != nil &&
c.buildSpec.Source.Container.Image != "" {
return fmt.Errorf("both source URL and container image are specified, only one can be used at the same time")
}

if c.buildSpec.Source.URL == "" &&
c.buildSpec.Source.Container != nil &&
c.buildSpec.Source.Container.Image == "" {
return fmt.Errorf("no input source was specified, either source URL or container image needs to be provided")
}

return nil
}

Expand All @@ -56,6 +70,8 @@ func (c *CreateCommand) Run(params *params.Params, io *genericclioptions.IOStrea
b := &buildv1alpha1.Build{Spec: *c.buildSpec}
flags.SanitizeBuildSpec(&b.Spec)

b.Name = c.name

clientset, err := params.ShipwrightClientSet()
if err != nil {
return err
Expand All @@ -78,9 +94,6 @@ func createCmd() runner.SubCommand {
// instantiating command-line flags and the build-spec structure which receives the informed flag
// values, also marking certain flags as mandatory
buildSpecFlags := flags.BuildSpecFromFlags(cmd.Flags())
if err := cmd.MarkFlagRequired(flags.SourceURLFlag); err != nil {
panic(err)
}
if err := cmd.MarkFlagRequired(flags.OutputImageFlag); err != nil {
panic(err)
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/shp/cmd/build/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ package build
import (
"fmt"

buildv1alpha1 "github.com/shipwright-io/build/pkg/apis/build/v1alpha1"
"github.com/spf13/cobra"

v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/cli-runtime/pkg/genericclioptions"

buildv1alpha1 "github.com/shipwright-io/build/pkg/apis/build/v1alpha1"
"github.com/shipwright-io/cli/pkg/shp/cmd/runner"
"github.com/shipwright-io/cli/pkg/shp/params"
)
Expand Down
7 changes: 4 additions & 3 deletions pkg/shp/cmd/build/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import (
"fmt"
"text/tabwriter"

buildv1alpha1 "github.com/shipwright-io/build/pkg/apis/build/v1alpha1"
"github.com/shipwright-io/cli/pkg/shp/cmd/runner"
"github.com/shipwright-io/cli/pkg/shp/params"
"github.com/spf13/cobra"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/cli-runtime/pkg/genericclioptions"

buildv1alpha1 "github.com/shipwright-io/build/pkg/apis/build/v1alpha1"
"github.com/shipwright-io/cli/pkg/shp/cmd/runner"
"github.com/shipwright-io/cli/pkg/shp/params"
)

// ListCommand struct contains user input to the List subcommand of Build
Expand Down
13 changes: 6 additions & 7 deletions pkg/shp/cmd/build/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,19 @@ import (
"fmt"
"time"

"github.com/spf13/cobra"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/cli-runtime/pkg/genericclioptions"

buildv1alpha1 "github.com/shipwright-io/build/pkg/apis/build/v1alpha1"
buildclientset "github.com/shipwright-io/build/pkg/client/clientset/versioned"

"github.com/shipwright-io/cli/pkg/shp/cmd/runner"
"github.com/shipwright-io/cli/pkg/shp/flags"
"github.com/shipwright-io/cli/pkg/shp/params"
"github.com/shipwright-io/cli/pkg/shp/reactor"
"github.com/shipwright-io/cli/pkg/shp/tail"

"github.com/spf13/cobra"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/cli-runtime/pkg/genericclioptions"
)

// RunCommand represents the `build run` sub-command, which creates a unique BuildRun instance to run
Expand Down
Loading

0 comments on commit 20e1b46

Please sign in to comment.