diff --git a/go.mod b/go.mod index 66f11b1c9..dd631b42a 100644 --- a/go.mod +++ b/go.mod @@ -37,6 +37,7 @@ require ( github.com/gobwas/glob v0.2.3 github.com/google/go-containerregistry v0.19.2 github.com/google/go-github/v32 v32.1.0 + github.com/google/go-github/v39 v39.2.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/uuid v1.6.0 github.com/henvic/httpretty v0.1.3 diff --git a/go.sum b/go.sum index babed26fc..49a896dde 100644 --- a/go.sum +++ b/go.sum @@ -661,6 +661,8 @@ github.com/google/go-containerregistry v0.19.2 h1:TannFKE1QSajsP6hPWb5oJNgKe1IKj github.com/google/go-containerregistry v0.19.2/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI= github.com/google/go-github/v32 v32.1.0 h1:GWkQOdXqviCPx7Q7Fj+KyPoGm4SwHRh8rheoPhd27II= github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI= +github.com/google/go-github/v39 v39.2.0 h1:rNNM311XtPOz5rDdsJXAp2o8F67X9FnROXTvto3aSnQ= +github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE= github.com/google/go-intervals v0.0.2 h1:FGrVEiUnTRKR8yE04qzXYaJMtnIYqobR5QbblK3ixcM= github.com/google/go-intervals v0.0.2/go.mod h1:MkaR3LNRfeKLPmqgJYs4E66z5InYjmCjbbr4TQlcT6Y= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= @@ -1318,6 +1320,7 @@ golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= @@ -1622,6 +1625,7 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= diff --git a/internal/cli/kraft/cloud/deploy/deployer.go b/internal/cli/kraft/cloud/deploy/deployer.go index 287bdc6de..8bda1ab1c 100644 --- a/internal/cli/kraft/cloud/deploy/deployer.go +++ b/internal/cli/kraft/cloud/deploy/deployer.go @@ -40,6 +40,7 @@ type deployer interface { func deployers() []deployer { return []deployer{ &deployerImageName{}, + &deployerKraftfileRepo{}, &deployerKraftfileRuntime{}, &deployerKraftfileUnikraft{}, } diff --git a/internal/cli/kraft/cloud/deploy/deployer_kraftfile_github.go b/internal/cli/kraft/cloud/deploy/deployer_kraftfile_github.go new file mode 100644 index 000000000..5fc3fd188 --- /dev/null +++ b/internal/cli/kraft/cloud/deploy/deployer_kraftfile_github.go @@ -0,0 +1,204 @@ +package deploy + +import ( + "context" + "fmt" + "path/filepath" + "sort" + "strings" + + "github.com/google/go-github/v39/github" + "golang.org/x/oauth2" + + "kraftkit.sh/config" + "kraftkit.sh/internal/ghrepo" + "kraftkit.sh/manifest" + "kraftkit.sh/pack" + kcclient "sdk.kraft.cloud/client" + kcinstances "sdk.kraft.cloud/instances" + kcservices "sdk.kraft.cloud/services" +) + +const treeSeparator = "/tree/" + +type deployerKraftfileRepo struct { + args []string + url string +} + +func (d *deployerKraftfileRepo) Name() string { + return "kraftfile-repo" +} + +func (d *deployerKraftfileRepo) String() string { + if len(d.args) == 0 { + return "run the given link with a Kraftfile" + } + + return fmt.Sprintf("run the detected Kraftfile in the given link after cloning and use '%s' as arg(s)", strings.Join(d.args, " ")) +} + +func (d *deployerKraftfileRepo) Deployable(ctx context.Context, opts *DeployOptions, args ...string) (bool, error) { + url := args[0] + + if !strings.Contains(url, "github.com") { + return false, nil + } + + if strings.Contains(url, treeSeparator) { + url = strings.Split(url, treeSeparator)[0] + } + + _, err := ghrepo.NewFromURL(url) + if err != nil { + return false, err + } + + d.url = args[0] + d.args = args[1:] + + return true, nil +} + +// getAllBranchesSorted returns all branches of a given repository sorted +// by size in descending order. +// If no token is specified, it will only have access to public repositories +func getAllBranchesSorted(ctx context.Context, owner, repo, token string) ([]string, error) { + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: token}, + ) + tc := oauth2.NewClient(ctx, ts) + + client := github.NewClient(tc) + + var allBranches []*github.Branch + opt := &github.BranchListOptions{ListOptions: github.ListOptions{PerPage: 100}} + for { + branches, resp, err := client.Repositories.ListBranches(ctx, owner, repo, opt) + if err != nil { + return nil, err + } + allBranches = append(allBranches, branches...) + if resp.NextPage == 0 { + break + } + opt.Page = resp.NextPage + } + + var branchNames []string + for _, branch := range allBranches { + branchNames = append(branchNames, branch.GetName()) + } + + // Sort all branches names by size in descending order + // This is done to ensure that the longest name is the first one + sort.Slice(branchNames, func(i, j int) bool { + return len(branchNames[i]) > len(branchNames[j]) + }) + + return branchNames, nil +} + +func (d *deployerKraftfileRepo) Deploy(ctx context.Context, opts *DeployOptions, _ ...string) (*kcclient.ServiceResponse[kcinstances.GetResponseItem], *kcclient.ServiceResponse[kcservices.GetResponseItem], error) { + var err error + var ghProvider manifest.Provider + + link := d.url + branch := "" + path := "." + + if strings.Contains(d.url, treeSeparator) { + split1 := strings.SplitN(d.url, treeSeparator, 2) + link = split1[0] + } + + repo, err := ghrepo.NewFromURL(link) + if err != nil { + return nil, nil, err + } + + if strings.Contains(d.url, treeSeparator) { + branchPath := strings.SplitN(d.url, treeSeparator, 2)[1] + + token := "" + for key, auth := range config.G[config.KraftKit](ctx).Auth { + if auth.Endpoint == "github.com" || key == "github.com" { + token = auth.Token + break + } + } + branches, err := getAllBranchesSorted(ctx, repo.RepoOwner(), repo.RepoName(), token) + if err != nil { + return nil, nil, err + } + + for _, branchName := range branches { + if strings.HasPrefix(branchPath, branchName) { + branch = branchName + break + } + } + + if branch == "" { + return nil, nil, fmt.Errorf("could not match branch from given url, are you sure the url is correct?") + } + + path = strings.SplitN(branchPath, branch+"/", 2)[1] + } + + ghProvider, err = manifest.NewGitHubProvider( + ctx, + link, + manifest.WithAuthConfig(config.G[config.KraftKit](ctx).Auth), + manifest.WithUpdate(true)) + if err != nil { + return nil, nil, err + } + + var m *manifest.Manifest = &manifest.Manifest{ + Type: "app", + Name: repo.RepoName(), + Origin: link, + Provider: ghProvider, + Channels: []manifest.ManifestChannel{ + { + Name: branch, + Default: true, + Resource: link, + }, + }, + } + + p, err := manifest.NewPackageFromManifest( + m, + manifest.WithAuthConfig(config.G[config.KraftKit](ctx).Auth), + manifest.WithUpdate(true), + ) + if err != nil { + return nil, nil, err + } + + err = p.Pull( + ctx, + pack.WithPullWorkdir(opts.Workdir), + pack.WithPullUnstructured(true), + ) + if err != nil { + return nil, nil, err + } + + opts.Workdir = filepath.Join(opts.Workdir, repo.RepoName(), path) + + deployers := []deployer{ + &deployerKraftfileRuntime{}, + &deployerKraftfileUnikraft{}, + } + + for _, deployer := range deployers { + if deployable, _ := deployer.Deployable(ctx, opts, d.args...); deployable { + return deployer.Deploy(ctx, opts, d.args...) + } + } + + return nil, nil, fmt.Errorf("no deployer found for the given project link") +} diff --git a/manifest/directory.go b/manifest/directory.go index 116629dee..cd4a34dfa 100644 --- a/manifest/directory.go +++ b/manifest/directory.go @@ -101,13 +101,16 @@ func (dp DirectoryProvider) PullManifest(ctx context.Context, manifest *Manifest return fmt.Errorf("cannot determine channel for directory provider") } - local, err := unikraft.PlaceComponent( - popts.Workdir(), - manifest.Type, - manifest.Name, - ) - if err != nil { - return fmt.Errorf("could not place component package: %s", err) + local := manifest.Name + if !popts.Unstructured() { + local, err = unikraft.PlaceComponent( + popts.Workdir(), + manifest.Type, + manifest.Name, + ) + if err != nil { + return fmt.Errorf("could not place component package: %s", err) + } } f, err := os.Lstat(local) diff --git a/manifest/pack_pull_archive.go b/manifest/pack_pull_archive.go index b05faf9ca..399093c8e 100644 --- a/manifest/pack_pull_archive.go +++ b/manifest/pack_pull_archive.go @@ -206,13 +206,16 @@ func pullArchive(ctx context.Context, manifest *Manifest, opts ...pack.PullOptio local := cache if len(popts.Workdir()) > 0 { - local, err = unikraft.PlaceComponent( - popts.Workdir(), - manifest.Type, - manifest.Name, - ) - if err != nil { - return fmt.Errorf("could not place component package: %s", err) + local = manifest.Name + if !popts.Unstructured() { + local, err = unikraft.PlaceComponent( + popts.Workdir(), + manifest.Type, + manifest.Name, + ) + if err != nil { + return fmt.Errorf("could not place component package: %s", err) + } } } diff --git a/manifest/pack_pull_git.go b/manifest/pack_pull_git.go index 7314bf3aa..10c026953 100644 --- a/manifest/pack_pull_git.go +++ b/manifest/pack_pull_git.go @@ -184,13 +184,16 @@ func pullGit(ctx context.Context, manifest *Manifest, opts ...pack.PullOption) e copts.ReferenceName = gitplumbing.NewBranchReferenceName(version) } - local, err := unikraft.PlaceComponent( - popts.Workdir(), - manifest.Type, - manifest.Name, - ) - if err != nil { - return fmt.Errorf("could not place component package: %w", err) + local := manifest.Name + if !popts.Unstructured() { + local, err = unikraft.PlaceComponent( + popts.Workdir(), + manifest.Type, + manifest.Name, + ) + if err != nil { + return fmt.Errorf("could not place component package: %w", err) + } } entry := log.G(ctx). diff --git a/pack/pull_options.go b/pack/pull_options.go index 44fe47fe9..4f9955248 100644 --- a/pack/pull_options.go +++ b/pack/pull_options.go @@ -16,6 +16,7 @@ type PullOptions struct { onProgress func(progress float64) workdir string useCache bool + unstructured bool } // Auths returns the set authentication config for a given domain or nil if the @@ -53,6 +54,11 @@ func (ppo *PullOptions) UseCache() bool { return ppo.useCache } +// Unstructured returns whether the pull should happen to the workdir directly. +func (ppo *PullOptions) Unstructured() bool { + return ppo.unstructured +} + // PullOption is an option function which is used to modify PullOptions. type PullOption func(opts *PullOptions) error @@ -120,3 +126,12 @@ func WithPullCache(cache bool) PullOption { return nil } } + +// WithPullUnstructured to set whether the pull should happen to the workdir +// directly. +func WithPullUnstructured(unstructured bool) PullOption { + return func(opts *PullOptions) error { + opts.unstructured = unstructured + return nil + } +}