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

Add ImageTxt collection #86

Closed
wants to merge 4 commits into from
Closed
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
23 changes: 23 additions & 0 deletions cmd/hauler/cli/store/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/rancherfederal/hauler/pkg/apis/hauler.cattle.io/v1alpha1"
tchart "github.com/rancherfederal/hauler/pkg/collection/chart"
"github.com/rancherfederal/hauler/pkg/collection/imagetxt"
"github.com/rancherfederal/hauler/pkg/collection/k3s"
"github.com/rancherfederal/hauler/pkg/content"
"github.com/rancherfederal/hauler/pkg/log"
Expand Down Expand Up @@ -143,6 +144,28 @@ func SyncCmd(ctx context.Context, o *SyncOpts, s *store.Store) error {
}
}

case v1alpha1.ImageTxtsContentKind:
var cfg v1alpha1.ImageTxts
if err := yaml.Unmarshal(doc, &cfg); err != nil {
return err
}

for _, cfgIt := range cfg.Spec.ImageTxts {
it, err := imagetxt.New(
imagetxt.WithContext(ctx),
imagetxt.WithRef(cfgIt.Ref),
imagetxt.WithIncludeSources(cfgIt.Sources.Include...),
imagetxt.WithExcludeSources(cfgIt.Sources.Exclude...),
)
if err != nil {
return fmt.Errorf("convert ImageTxt %s: %v", cfg.Name, err)
}

if _, err := s.AddCollection(ctx, it); err != nil {
return fmt.Errorf("add ImageTxt %s to store: %v", cfg.Name, err)
}
}

default:
return fmt.Errorf("unrecognized content/collection type: %s", obj.GroupVersionKind().String())
}
Expand Down
30 changes: 30 additions & 0 deletions pkg/apis/hauler.cattle.io/v1alpha1/imagetxt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package v1alpha1

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

const (
ImageTxtsContentKind = "ImageTxts"
)

type ImageTxts struct {
*metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec ImageTxtsSpec `json:"spec,omitempty"`
}

type ImageTxtsSpec struct {
ImageTxts []ImageTxt `json:"imageTxts,omitempty"`
}

type ImageTxt struct {
Ref string `json:"ref,omitempty"`
Sources ImageTxtSources `json:"sources,omitempty"`
}

type ImageTxtSources struct {
Include []string `json:"include,omitempty"`
Exclude []string `json:"exclude,omitempty"`
}
2 changes: 0 additions & 2 deletions pkg/artifact/local/layer.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import (
"github.com/rancherfederal/hauler/pkg/artifact/types"
)

type Opener func() (io.ReadCloser, error)

func LayerFromOpener(opener Opener, opts ...LayerOption) (v1.Layer, error) {
var err error

Expand Down
25 changes: 25 additions & 0 deletions pkg/artifact/local/opener.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package local

import (
"io"
"net/http"
"os"
)

type Opener func() (io.ReadCloser, error)

func LocalOpener(path string) Opener {
return func() (io.ReadCloser, error) {
return os.Open(path)
}
}

func RemoteOpener(url string) Opener {
return func() (io.ReadCloser, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
return resp.Body, nil
}
}
264 changes: 264 additions & 0 deletions pkg/collection/imagetxt/imagetxt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
package imagetxt

import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"strings"
"sync"

"github.com/rancherfederal/hauler/pkg/artifact"
"github.com/rancherfederal/hauler/pkg/artifact/local"
"github.com/rancherfederal/hauler/pkg/content/image"
"github.com/rancherfederal/hauler/pkg/log"

"github.com/google/go-containerregistry/pkg/name"
)

type ImageTxt struct {
Ref string
IncludeSources map[string]bool
ExcludeSources map[string]bool

lock *sync.Mutex
ctx context.Context

getter local.Opener
computed bool
contents map[name.Reference]artifact.OCI
}

var _ artifact.Collection = (*ImageTxt)(nil)

type Option interface {
Apply(*ImageTxt) error
}

type withRef string

func (o withRef) Apply(it *ImageTxt) error {
ref := string(o)

if strings.HasPrefix(ref, "http") || strings.HasPrefix(ref, "https") {
it.getter = local.RemoteOpener(ref)
} else {
it.getter = local.LocalOpener(ref)
}
return nil
}

func WithRef(ref string) Option {
return withRef(ref)
}

type withIncludeSources []string

func (o withIncludeSources) Apply(it *ImageTxt) error {
if it.IncludeSources == nil {
it.IncludeSources = make(map[string]bool)
}
for _, s := range o {
it.IncludeSources[s] = true
}
return nil
}

func WithIncludeSources(include ...string) Option {
return withIncludeSources(include)
}

type withExcludeSources []string

func (o withExcludeSources) Apply(it *ImageTxt) error {
if it.ExcludeSources == nil {
it.ExcludeSources = make(map[string]bool)
}
for _, s := range o {
it.ExcludeSources[s] = true
}
return nil
}

func WithExcludeSources(exclude ...string) Option {
return withExcludeSources(exclude)
}

// TODO - pass in context in a better way
// TODO - maybe Collection interface needs redefinition?

type withContext struct{ context.Context }

func (o withContext) Apply(it *ImageTxt) error {
it.ctx = o.Context
return nil
}

func WithContext(ctx context.Context) Option {
return withContext{Context: ctx}
}

func New(opts ...Option) (*ImageTxt, error) {
it := &ImageTxt{
lock: &sync.Mutex{},
}

for i, o := range opts {
if err := o.Apply(it); err != nil {
return nil, fmt.Errorf("invalid option %d: %v", i, err)
}
}

return it, nil
}

func (it *ImageTxt) Contents() (map[name.Reference]artifact.OCI, error) {
it.lock.Lock()
defer it.lock.Unlock()
if !it.computed {
if err := it.compute(); err != nil {
return nil, fmt.Errorf("compute OCI layout: %v", err)
}
it.computed = true
}
return it.contents, nil
}

func (it *ImageTxt) compute() error {
l := log.FromContext(it.ctx)

it.contents = make(map[name.Reference]artifact.OCI)

r, err := it.getter()
if err != nil {
return fmt.Errorf("fetch image.txt ref %s: %v", it.Ref, err)
}
defer r.Close()

buf := &bytes.Buffer{}
if _, err := io.Copy(buf, r); err != nil {
return fmt.Errorf("read image.txt ref %s: %v", it.Ref, err)
}

entries, err := splitImagesTxt(buf)
if err != nil {
return fmt.Errorf("parse image.txt ref %s: %v", it.Ref, err)
}

foundSources := make(map[string]bool)
for _, e := range entries {
for s := range e.Sources {
foundSources[s] = true
}
}

var pullAll bool
var targetSources map[string]bool

if len(foundSources) == 0 || (len(it.IncludeSources) == 0 && len(it.ExcludeSources) == 0) {
// pull all found images
pullAll = true

if len(foundSources) == 0 {
l.Infof("image txt file appears to have no sources; pulling all found images")
if len(it.IncludeSources) != 0 || len(it.ExcludeSources) != 0 {
l.Warnf("ImageTxt provided include or exclude sources; ignoring")
}
} else if len(it.IncludeSources) == 0 && len(it.ExcludeSources) == 0 {
l.Infof("image-sources txt file not filtered; pulling all found images")
}
} else {
// determine sources to pull
if len(it.IncludeSources) != 0 && len(it.ExcludeSources) != 0 {
l.Warnf("ImageTxt provided include and exclude sources; using only include sources")
}

if len(it.IncludeSources) != 0 {
targetSources = it.IncludeSources
} else {
for s := range foundSources {
targetSources[s] = true
}
for s := range it.ExcludeSources {
delete(targetSources, s)
}
}
var targetSourcesArr []string
for s := range targetSources {
targetSourcesArr = append(targetSourcesArr, s)
}
l.Infof("including images covering sources %s", strings.Join(targetSourcesArr, ", "))
}

for _, e := range entries {
var matchesSourceFilter bool
if pullAll {
l.Infof("marked image %s for pull", e.Reference)
} else {
for s := range e.Sources {
if targetSources[s] {
matchesSourceFilter = true
l.Infof("marked image %s for pull, matched source %s", e.Reference, s)
break
}
}
}

if pullAll || matchesSourceFilter {
curImage, err := image.NewImage(e.Reference.String())
if err != nil {
return fmt.Errorf("pull image %s: %v", e.Reference, err)
}
it.contents[e.Reference] = curImage
}
}

return nil
}

type imageTxtEntry struct {
Reference name.Reference
Sources map[string]bool
}

func splitImagesTxt(r io.Reader) ([]imageTxtEntry, error) {
var entries []imageTxtEntry
scanner := bufio.NewScanner(r)
for scanner.Scan() {
curEntry := imageTxtEntry{
Sources: make(map[string]bool),
}

lineContent := scanner.Text()
if lineContent == "" || strings.HasPrefix(lineContent, "#") {
// skip past empty and commented lines
continue
}
splitContent := strings.Split(lineContent, " ")
if len(splitContent) > 2 {
return nil, fmt.Errorf(
"invalid image.txt format: must contain only an image reference and sources separated by space; invalid line: %q",
lineContent)
}

curRef, err := name.ParseReference(splitContent[0])
if err != nil {
return nil, fmt.Errorf("invalid reference %s: %v", splitContent[0], err)
}
curEntry.Reference = curRef

if len(splitContent) == 2 {
for _, source := range strings.Split(splitContent[1], ",") {
curEntry.Sources[source] = true
}
}

entries = append(entries, curEntry)
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("scan contents: %v", err)
}

return entries, nil
}
5 changes: 5 additions & 0 deletions pkg/store/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/rancherfederal/hauler/pkg/artifact"
"github.com/rancherfederal/hauler/pkg/cache"
"github.com/rancherfederal/hauler/pkg/layout"
"github.com/rancherfederal/hauler/pkg/log"
)

// AddArtifact will add an artifact.OCI to the store
Expand All @@ -20,6 +21,10 @@ import (
// strict types to define generic content, but provides a processing pipeline suitable for extensibility. In the
// future we'll allow users to define their own content that must adhere either by artifact.OCI or simply an OCI layout.
func (s *Store) AddArtifact(ctx context.Context, oci artifact.OCI, reference name.Reference) (ocispec.Descriptor, error) {
l := log.FromContext(ctx)

l.Infof("adding ref %s to store", reference.String())

if err := s.precheck(); err != nil {
return ocispec.Descriptor{}, err
}
Expand Down
Loading