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

Initial implementation of state checkout --from-archive. #3444

Merged
merged 20 commits into from
Aug 16, 2024

Conversation

mitchell-as
Copy link
Contributor

@mitchell-as mitchell-as commented Aug 12, 2024

StoryDX-2981 `state checkout` can install from an offline source

We needed a project.json file, similar to offline installers, so here's a sample of the one I'm using in integration tests:

{
	"org_name": "ActiveState-CLI",
	"project_name": "AlmostEmpty",
	"commit_id": "6cd1cc1f-3886-439b-8373-c24ca06ab150",
	"branch": "main",
	"platform_id": "78977bc8-0f32-519d-80f3-9043f059398c"
}

@mitchell-as mitchell-as force-pushed the mitchell/dx-2981 branch 2 times, most recently from c2d9352 to c896512 Compare August 13, 2024 15:09
@mitchell-as
Copy link
Contributor Author

The lone test failure is a timeout unrelated to this PR.

Comment on lines 43 to 47
{
Name: "from-archive",
Description: locale.Tl("flag_state_checkout_from_archive", "Checkout from the given .tar.gz archive"),
Value: &params.FromArchive,
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed the manual verification you're doing where essentially either namespace is required OR this flag is. This makes me think maybe we should treat the namespace arg more intelligently. eg. if you specify a filepath for the namespace then we use that. What do you think?

Since we use a special type for this arg we could introduce a new type that multiplexes to either namespace or filepath.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds reasonable, as long as the archive path always has an extension. Otherwise the argument is ambiguous, as it can be either org/project, project, or path/to/tar.gz.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, I don't see any reason why it wouldn't have an extension.

// readProject reads and returns a project namespace (with commitID) and branch from
// "project.json", as well as a platformID.
func readProject(dir string) (*project.Namespaced, string, strfmt.UUID, error) {
projectBytes, err := fileutils.ReadFile(filepath.Join(dir, "project.json"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make project.json a constant, and rename it to installer_config.json.

return nil, errs.Wrap(err, "Invalid archive: buildplan.json not found")
}

return buildplan.Unmarshal(buildplanBytes)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still need to wrap this error.

commitID := ns.CommitID
var language string
if !fromArchive {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets extract the logic under this to its own function, as the bulk of the current function is inside this logic statement.

Comment on lines 180 to 185
var buildPlan *buildplan.BuildPlan
if opts.Archive != nil {
buildPlan = opts.Archive.BuildPlan
} else if opts.Commit != nil {
buildPlan = opts.Commit.BuildPlan()
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest making this a switch case, and making the default behaviour the logic from line 187. Just so the relations are clearly communicated.

); err != nil {
}
if opts.Archive != nil {
rtOpts = append(rtOpts, runtime.WithFromArchive(opts.Archive.Dir, opts.Archive.PlatformID, checkout.ArtifactExt))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can rename this one to WithArchive imo. The "From" isn't doing anything but trigger my OCD :p

func New(repo git.Repository, prime primeable) *Checkout {
return &Checkout{repo, prime.Output(), prime.Config(), prime.Analytics(), "", prime.Auth()}
}

func (r *Checkout) Run(ns *project.Namespaced, branchName, cachePath, targetPath string, noClone bool) (_ string, rerr error) {
func (r *Checkout) Run(ns *project.Namespaced, branchName, cachePath, targetPath string, noClone, fromArchive bool) (_ string, rerr error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The checkout runbit doesn't seem to actually care about archives. I think we should name this according to the behaviour of the package, not according to the use-case we have external from this package. So perhaps we can name this validate, as that's what the logic seems to do (with a little bit of cleanup sprinkled in there, so maybe validateAndCleanup?).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've gone with onlyValidate. With all of the requested changes, this makes sense to me, but let me know if you disagree and we can reconsider your suggestions.

Comment on lines 44 to 47
// Options for setting up a runtime from an archive.
FromArchiveDir string
PlatformID *strfmt.UUID
ArtifactExt string
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make a struct to put these under. As they are all directly related and should never be used independently from one another.

Comment on lines 290 to 297
var err error
name := artifact.ArtifactID.String() + s.opts.ArtifactExt
artifactFile := filepath.Join(s.opts.FromArchiveDir, name)
logging.Debug("Reading file '%s' for '%s'", artifactFile, artifact.DisplayName)
b, err = fileutils.ReadFile(artifactFile)
if err != nil {
return errs.Wrap(err, "read from archive failed")
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think since this is bypassing the event reporting of downloads we will have a progress indicator for downloads that never moves.

We'll want to ensure that those events are still send, but we probably also want to update our runbit logic so that instead of "Downloading" we say "Unpacking" when using the archive approach.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I'll add the download events. In testing, this step went by so fast I didn't even see a download bar. I only saw a brief "Unpacking" bar since setting up from an archive does go through this step.

cp.Expect("Installing")
cp.Expect("All dependencies have been installed and verified")
cp.Expect("Checked out project ActiveState-CLI/AlmostEmpty")
cp.ExpectExitCode(0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also assert that the resulting runtime has some of the expected files. This test currently is just testing the UI/UX of the command itself, we should also verify that the end result is what we expect it to be.

Now the namespace argument can be either
- org/project
- project (org is inferred)
- archive.tar.gz
@mitchell-as mitchell-as force-pushed the mitchell/dx-2981 branch 2 times, most recently from dfbedd0 to cbcfa75 Compare August 14, 2024 22:34
@mitchell-as mitchell-as marked this pull request as ready for review August 14, 2024 22:47
Comment on lines 99 to 101
// checkout performs the checkout and returns the checkout's owner, project, commitID, branchName,
// and language.
func (r *Checkout) checkout(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't feel right to call this checkout. That's what the main Run function does (ie. creating the project file is effectively the checkout).

Suggest turning this into a function called sanitize() that does all the cleaning up of parameters, and then separately call the clone behaviour from Run().

I'm not a big fan of the number of parameters this returns, but I also don't see an obvious solution that doesn't add boilerplate that I'm even less of a fan of. Just flagging it in case you have ideas.

Copy link
Contributor Author

@mitchell-as mitchell-as Aug 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've gone with fetchProject since I feel like that is the goal of this function. Fetch the project from the platform and return its complete set of parameters. If you don't like it, we'll go with sanitize.

}

func (r *Checkout) Run(ns *project.Namespaced, branchName, cachePath, targetPath string, noClone, fromArchive bool) (_ string, rerr error) {
func (r *Checkout) Run(ns *project.Namespaced, branchName, cachePath, targetPath string, noClone, onlyValidate bool) (_ string, rerr error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onlyValidate doesn't seem an appropriate name to me. If supplied it in fact does no validations, and it only does the checkout (ie. create the project files).

Perhaps bareCheckout would be more appropriate?

Copy link
Contributor Author

@mitchell-as mitchell-as Aug 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. I don't want to spend too much time on a name.

Comment on lines 93 to 94
err := ns.Set(params.Namespace)
if err != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
err := ns.Set(params.Namespace)
if err != nil {
if err := ns.Set(params.Namespace); err != nil {

if err != nil {
return errs.Wrap(err, "Unable to read archive")
}
defer archive.Cleanup()
params.Namespace = archive.Namespace
ns = *archive.Namespace
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ns = *archive.Namespace
ns = archive.Namespace

Doesn't seem work dereferencing when all your use of it doesn't seem to care, and in fact you create a reference again when passing it to checkout.Run(). May as well avoid the potential nil pointer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ns cannot be a pointer because of the ns.Set() function used earlier. The dereference here is needed, as is the passing of &ns to checkout.Run(). It's a product of our clever use the first argument being either a namespace or archive file.

opts.ArtifactExt = ext
}
func WithArchive(dir string, platformID strfmt.UUID, ext string) SetOpt {
return func(opts *Opts) { opts.FromArchive = &fromArchive{dir, platformID, ext} }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return func(opts *Opts) { opts.FromArchive = &fromArchive{dir, platformID, ext} }
return func(opts *Opts) {
opts.FromArchive = &fromArchive{dir, platformID, ext}
}


if err := s.fireEvent(events.ArtifactDownloadSuccess{artifact.ArtifactID}); err != nil {
return errs.Wrap(errs.Pack(err, err), "Could not handle ArtifactDownloadSuccess event")
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realized we are in fact handling unpack events separately. So I think it makes more sense that we don't send the download events and instead set the toDownload counter to 0 if we are installing from an archive, ie.

artifactsToDownload := artifactsToInstall.Filter(func(a *buildplan.Artifact) bool {

Apologies, I didn't have this in mind with my initial review.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, well that complicated things. Let me know if my solution is sub-optimal.

proj, err := project.FromPath(filepath.Join(ts.Dirs.Work, "AlmostEmpty"))
suite.Require().NoError(err)
cachePath := filepath.Join(ts.Dirs.Cache, runtime_helpers.DirNameFromProjectDir(proj.Dir()))
zlibH := filepath.Join(cachePath, "usr", "include", "zlib.h")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure this will be different on mac/windows.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, it can be whatever I say it is since I produced the test archives :) But seriously, I untarred the ingredients and looked for a common one that had the same path and used that. The tests on all three platforms are passing, so it's all good.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh right, I forgot about that detail :p Ok we'll worry about this once we can test with proper payloads.

@mitchell-as mitchell-as force-pushed the mitchell/dx-2981 branch 2 times, most recently from 9ea49f1 to 08ebfdf Compare August 15, 2024 19:36
@mitchell-as
Copy link
Contributor Author

Closing in favor of #3451

@Naatan Naatan reopened this Aug 16, 2024
@Naatan
Copy link
Member

Naatan commented Aug 16, 2024

@mitchell-as reopened as that other ticket was targeting this one so the review process was still workable. Though given I'm about to approve I guess this is just busywork 😅 Anyway now you have a signal to move the story along.

@mitchell-as mitchell-as reopened this Aug 16, 2024
@mitchell-as mitchell-as merged commit 34cf30c into version/0-46-0-RC1 Aug 16, 2024
13 of 18 checks passed
@mitchell-as mitchell-as deleted the mitchell/dx-2981 branch August 16, 2024 18:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants