Skip to content

Commit

Permalink
feat(push): adds build and push functionality into different subcomma…
Browse files Browse the repository at this point in the history
…nds (#21)

* feat(push): adds build and push functionality into different subcommands

Closes #6

Signed-off-by: Jennifer Power <barnabei.jennifer@gmail.com>

* docs: updates README.md with new usage directions for push

Signed-off-by: Jennifer Power <barnabei.jennifer@gmail.com>

* feat: changes Client interface to allow the descriptors be returned

Returning the descriptors allows for easier testing of concrete
implementations of the Client interface and allows for post-processing
on descriptors.

Signed-off-by: Jennifer Power <barnabei.jennifer@gmail.com>

* chore: comment clean up in graph and builder packages

Signed-off-by: Jennifer Power <barnabei.jennifer@gmail.com>

* chore: update README.md with user workflow

Signed-off-by: Jennifer Power <barnabei.jennifer@gmail.com>

* test: adds artifact digest checking on build unit tests

Signed-off-by: Jennifer Power <barnabei.jennifer@gmail.com>

* chore: adds logger to push command

Signed-off-by: Jennifer Power <barnabei.jennifer@gmail.com>

* test(cli): adds logger to push e2e test

Signed-off-by: Jennifer Power <barnabei.jennifer@gmail.com>

* Update README.md

Co-authored-by: Andrew Block <andy.block@gmail.com>

* docs: adds correct output flags and fixes naming in examples

Signed-off-by: Jennifer Power <barnabei.jennifer@gmail.com>

* fix(build): bumps log about workspace to Info level in build command

Signed-off-by: Jennifer Power <barnabei.jennifer@gmail.com>

Co-authored-by: Andrew Block <andy.block@gmail.com>
  • Loading branch information
jpower432 and sabre1041 committed Jun 7, 2022
1 parent a507d0b commit d3bd414
Show file tree
Hide file tree
Showing 23 changed files with 622 additions and 141 deletions.
33 changes: 30 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,43 @@ embedded in UOR artifacts.

To learn more about Universal Runtime visit the UOR Framework website at https://uor-framework.github.io.

## Development

### Requirements

- `go` version 1.17+

### Build

```
make
./bin/client -h
```
### Test

#### Unit:
```
make test-unit
```

## Basic Usage

### User Workflow

1. Create a directory with artifacts to publish to a registry as an OCI artifact. If the files reference each other, the client will replace the in-content linked files with the content address.
> WARNING: Currently, only JSON is supported for link replacement.
2. Use the `client build` command to create the output workspace with the rendered content. If the files in the workspace do not contain links to each other, skip this step.
3. Use the `client push` command to publish the workspace to a registry as an OCI artifact.
### Template content in a directory without pushing
```
# The default workspace is "client-workspace" in the current working directory
client build <directory> --output my-workspace
client build my-directory --output my-workspace
```

### Template content in a directory and push to a registry location
`client build <directory> --push --destination localhost:5000/myartifacts:latest`
### Push workspace to a registry location
```
client push my-workspace localhost:5000/myartifacts:latest
```



4 changes: 2 additions & 2 deletions builder/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ type Builder struct {
Source workspace.Workspace
}

// NewBuilder creates an new Builder from the source
// workspace
// NewBuilder creates a new Builder from the source
// workspace.
func NewBuilder(source workspace.Workspace) Builder {
return Builder{source}
}
Expand Down
4 changes: 2 additions & 2 deletions builder/graph/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ func (g *Graph) AddEdge(origin, destination string) error {
}

// Root calculates to root node of the graph.
// This is calculated base on existing child nodes.
// This expected only of root node to be found.
// This is calculated based on existing child nodes.
// This expects only one root node to be found.
func (g *Graph) Root() (*Node, error) {
// FIXME(jpowe432): Optimize or redesign the chain

Expand Down
102 changes: 13 additions & 89 deletions cli/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,37 +13,32 @@ import (
"github.com/uor-framework/client/builder"
"github.com/uor-framework/client/builder/graph"
"github.com/uor-framework/client/builder/parser"
"github.com/uor-framework/client/registryclient/orasclient"
"github.com/uor-framework/client/util/workspace"
)

type BuildOptions struct {
*RootOptions
Destination string
RootDir string
Insecure bool
PlainHTTP bool
Configs []string
Output string
Push bool
RootDir string
Output string
}

var clientBuildExamples = templates.Examples(
`
# Template content in a directory without pushing
client build <directory>
# Template content in a directory
# The default workspace is "client-workspace" in the current working directory.
client build my-directory
# Template content in a directory and push to a registry location
client build <directory> --push --destination localhost:5000/myartifacts:latest
# Template content into a specified output directory.
client build my-directory --output my-workspace
`,
)

func NewBuildCmd(rootOpts *RootOptions) *cobra.Command {
o := BuildOptions{RootOptions: rootOpts}

cmd := &cobra.Command{
Use: "build directory",
Short: "Template, build, and publish OCI content from a local directory",
Use: "build SRC",
Short: "Template and build files from a local directory into a UOR dataset",
Example: clientBuildExamples,
SilenceErrors: false,
SilenceUsage: false,
Expand All @@ -55,12 +50,7 @@ func NewBuildCmd(rootOpts *RootOptions) *cobra.Command {
},
}

cmd.Flags().StringArrayVarP(&o.Configs, "configs", "c", o.Configs, "auth config paths")
cmd.Flags().BoolVarP(&o.Insecure, "insecure", "", o.Insecure, "allow connections to SSL registry without certs")
cmd.Flags().BoolVarP(&o.PlainHTTP, "plain-http", "", o.PlainHTTP, "use plain http and not https")
cmd.Flags().StringVarP(&o.Output, "output", "o", o.Output, "location to stored templated files")
cmd.Flags().BoolVarP(&o.Push, "push", "p", o.Push, "push workspace artifacts to registry")
cmd.Flags().StringVarP(&o.Destination, "destination", "d", o.Destination, "image location to store artifacts in a registry")

return cmd
}
Expand All @@ -80,18 +70,11 @@ func (o *BuildOptions) Validate() error {
if _, err := os.Stat(o.RootDir); err != nil {
return fmt.Errorf("workspace directory %q: %v", o.RootDir, err)
}

if o.Push && o.Destination == "" {
return fmt.Errorf("destination must be set when using --push")

}

// TODO(jpower432): Validate the reference
return nil
}

func (o *BuildOptions) Run(ctx context.Context) error {
o.Logger.Debugf("Using output directory %q", o.Output)
o.Logger.Infof("Using output directory %q", o.Output)
userSpace, err := workspace.NewLocalWorkspace(o.RootDir)
if err != nil {
return err
Expand Down Expand Up @@ -181,72 +164,13 @@ func (o *BuildOptions) Run(ctx context.Context) error {
if err != nil {
return err
}

if err := templateBuilder.Run(ctx, g, renderSpace); err != nil {
return fmt.Errorf("error building content: %v", err)
}

if o.Push {
// Gather descriptors written to the render directory for publishing
client, err := orasclient.NewClient(
o.Destination,
orasclient.SkipTLSVerify(o.Insecure),
orasclient.WithPlainHTTP(o.PlainHTTP),
orasclient.WithAuthConfigs(o.Configs),
)
if err != nil {
return fmt.Errorf("error configuring client: %v", err)
}
var files []string
err = renderSpace.Walk(func(path string, info os.FileInfo, err error) error {
if err != nil {
return fmt.Errorf("traversing %s: %v", path, err)
}
if info == nil {
return fmt.Errorf("no file info")
}

if info.Mode().IsRegular() {
files = append(files, path)
}
return nil
})
if err != nil {
return err
}

// To allow the files to be loaded relative to the render
// workspace, change to the render directory. This is required
// to get path correct in the description annotations.
cwd, err := os.Getwd()
if err != nil {
return err
}
if err := os.Chdir(renderSpace.Path()); err != nil {
return err
}
defer func() {
if err := os.Chdir(cwd); err != nil {
o.Logger.Errorf("%v", err)
}
}()

descs, err := client.GatherDescriptors("", files...)
if err != nil {
return err
}

configDesc, err := client.GenerateConfig(nil)
if err != nil {
return err
}
_, _ = fmt.Fprintf(o.IOStreams.Out, "\nTo publish this content, run the following command:")
_, _ = fmt.Fprintf(o.IOStreams.Out, "\nclient push %s IMAGE\n", o.Output)

if err := client.GenerateManifest(configDesc, nil, descs...); err != nil {
return err
}

if err := client.Execute(ctx); err != nil {
return fmt.Errorf("error publishing content to %s: %v", o.Destination, err)
}
}
return nil
}
93 changes: 59 additions & 34 deletions cli/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@ import (
"context"
"fmt"
"io/ioutil"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"testing"

"github.com/google/go-containerregistry/pkg/registry"
"github.com/opencontainers/go-digest"
"github.com/stretchr/testify/require"
"github.com/uor-framework/client/cli/log"
"k8s.io/cli-runtime/pkg/genericclioptions"
Expand Down Expand Up @@ -70,29 +69,13 @@ func TestBuildValidate(t *testing.T) {
RootDir: "testdata",
},
},
{
name: "Valid/DestinationWithPush",
opts: &BuildOptions{
Destination: "test-registry.com/client-test:latest",
RootDir: "testdata",
Push: true,
},
},
{
name: "Invalid/RootDirDoesNotExist",
opts: &BuildOptions{
RootDir: "fake",
},
expError: "workspace directory \"fake\": stat fake: no such file or directory",
},
{
name: "Invalid/NoReferenceWithPush",
opts: &BuildOptions{
RootDir: "testdata",
Push: true,
},
expError: "destination must be set when using --push",
},
}

for _, c := range cases {
Expand All @@ -112,14 +95,10 @@ func TestBuildRun(t *testing.T) {
testlogr, err := log.NewLogger(ioutil.Discard, "debug")
require.NoError(t, err)

server := httptest.NewServer(registry.New())
t.Cleanup(server.Close)
u, err := url.Parse(server.URL)
require.NoError(t, err)

type spec struct {
name string
opts *BuildOptions
expected string
expError string
}

Expand All @@ -135,10 +114,9 @@ func TestBuildRun(t *testing.T) {
},
Logger: testlogr,
},
Destination: fmt.Sprintf("%s/client-test:latest", u.Host),
RootDir: "testdata/flatworkspace",
Push: true,
RootDir: "testdata/flatworkspace",
},
expected: "testdata/expected/flatworkspace",
},
{
name: "Success/MultiLevelWorkspace",
Expand All @@ -151,10 +129,9 @@ func TestBuildRun(t *testing.T) {
},
Logger: testlogr,
},
Destination: fmt.Sprintf("%s/client-test:latest", u.Host),
RootDir: "testdata/multi-level-workspace",
Push: true,
RootDir: "testdata/multi-level-workspace",
},
expected: "testdata/expected/multi-level-workspace",
},
{
name: "Success/UORParsing",
Expand All @@ -167,10 +144,24 @@ func TestBuildRun(t *testing.T) {
},
Logger: testlogr,
},
Destination: fmt.Sprintf("%s/client-test:latest", u.Host),
RootDir: "testdata/uor-template",
Push: true,
RootDir: "testdata/uor-template",
},
expected: "testdata/expected/uor-template",
},
{
name: "Failure/TwoRoots",
opts: &BuildOptions{
RootOptions: &RootOptions{
IOStreams: genericclioptions.IOStreams{
Out: os.Stdout,
In: os.Stdin,
ErrOut: os.Stderr,
},
Logger: testlogr,
},
RootDir: "testdata/tworoots",
},
expError: "error building content: error calculating root node: multiple roots found in graph: fish.jpg, fish2.jpg",
},
}

Expand All @@ -182,8 +173,42 @@ func TestBuildRun(t *testing.T) {
require.EqualError(t, err, c.expError)
} else {
require.NoError(t, err)
// TODO(jpower432): verify resulting this image is now pullable
actual := walkDir(t, c.opts.Output)
expected := walkDir(t, c.expected)

for path, data1 := range actual {
t.Log("checking path " + path)
data2, ok := expected[path]
require.True(t, ok)
require.Equal(t, digest.FromBytes(data2).String(), digest.FromBytes(data1).String())
}
}
})
}
}

func walkDir(t *testing.T, dir string) map[string][]byte {
files := map[string][]byte{}
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return fmt.Errorf("traversing %s: %v", path, err)
}
if info == nil {
return fmt.Errorf("no file info")
}

if info.IsDir() {
return nil
}

data, err := ioutil.ReadFile(path)
if err != nil {
return err
}
files[filepath.Base(path)] = data

return nil
})
require.NoError(t, err)
return files
}

0 comments on commit d3bd414

Please sign in to comment.