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 subcommand to remove existing SoftPack built environments #15

Merged
merged 22 commits into from
Nov 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4bd0e14
Add package to remove existing environments.
mjkw31 Nov 6, 2023
dcd4484
Config.CoreURL should point to the base URL, not the upload endpoint.
mjkw31 Nov 6, 2023
7288c86
Add command to remove existing environment.
mjkw31 Nov 6, 2023
96d103b
Correct GraphQL indentation.
mjkw31 Nov 7, 2023
5a838f5
Add RemoveFile method that converts a NoSuchKey error to an easier to…
mjkw31 Nov 8, 2023
de0d344
Add command to remove existing environment.
mjkw31 Nov 8, 2023
a60c7f3
Prepend base to path.
mjkw31 Nov 9, 2023
58d324e
Remove double-remove test as s3 client doesn't return an error when t…
mjkw31 Nov 9, 2023
eccd231
Add missing version to env path.
mjkw31 Nov 10, 2023
313c7f4
Confirm intent to remove envvironment before proceeding.
mjkw31 Nov 10, 2023
3fa3804
Corrected GraphQL JSON message.
mjkw31 Nov 10, 2023
384b73f
Make sure to not descend into child directories (which would inidcate…
mjkw31 Nov 10, 2023
4a14b7e
Add 'logging' to explain what each step is doing.
mjkw31 Nov 10, 2023
f6c295b
Reverted to correct JSON packet for variables in GraphQL mutation.
mjkw31 Nov 10, 2023
7d9ad45
Log that the directory is being removed as well.
mjkw31 Nov 10, 2023
69e72a2
Core does not like double slashes in the URL, so we have to remove a …
mjkw31 Nov 10, 2023
2eece26
Potentially need to trim slash suffix in artefact upload URL as well.
mjkw31 Nov 10, 2023
f95e594
De-lint.
mjkw31 Nov 10, 2023
72afcff
More Delinting.
mjkw31 Nov 10, 2023
3a51201
Confirm file existed, and was actually removed.
mjkw31 Nov 10, 2023
6df9e52
Corrected variable name.
mjkw31 Nov 10, 2023
91365d7
Changes following code review.
mjkw31 Nov 10, 2023
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ spack:
finalImage: "ubuntu:22.04"
processorTarget: "x86_64_v3"

coreURL: "http://x.y.z:9837/upload"
coreURL: "http://x.y.z:9837/softpack"
sb10 marked this conversation as resolved.
Show resolved Hide resolved
listenURL: "0.0.0.0:2456"
```

Expand Down
48 changes: 26 additions & 22 deletions build/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,15 @@ import (
)

const (
singularityDefBasename = "singularity.def"
exesBasename = "executables"
softpackYaml = "softpack.yml"
spackLock = "spack.lock"
builderOut = "builder.out"
SingularityDefBasename = "singularity.def"
ExesBasename = "executables"
SoftpackYaml = "softpack.yml"
SpackLockFile = "spack.lock"
BuilderOut = "builder.out"
moduleForCoreBasename = "module"
usageBasename = "README.md"
UsageBasename = "README.md"

uploadEndpoint = "/upload"
)

//go:embed singularity.tmpl
Expand Down Expand Up @@ -285,7 +287,7 @@ func (b *Builder) generateAndUploadSingularityDef(def *Definition, s3Path string
return "", err
}

singDefUploadPath := filepath.Join(s3Path, singularityDefBasename)
singDefUploadPath := filepath.Join(s3Path, SingularityDefBasename)

err = b.s3.UploadData(strings.NewReader(singDef), singDefUploadPath)

Expand Down Expand Up @@ -348,22 +350,22 @@ func (b *Builder) asyncBuild(def *Definition, wrInput, s3Path, singDef string) e
}

func (b *Builder) addLogToRepo(s3Path, environmentPath string) {
log, err := b.s3.OpenFile(filepath.Join(s3Path, builderOut))
log, err := b.s3.OpenFile(filepath.Join(s3Path, BuilderOut))
if err != nil {
slog.Error("error getting build log file", "err", err)

return
}

if err := b.addArtifactsToRepo(map[string]io.Reader{
builderOut: log,
BuilderOut: log,
}, environmentPath); err != nil {
slog.Error("error sending build log file to core", "err", err)
}
}

func (b *Builder) getExes(s3Path string) ([]string, error) {
exeData, err := b.s3.OpenFile(filepath.Join(s3Path, exesBasename))
exeData, err := b.s3.OpenFile(filepath.Join(s3Path, ExesBasename))
if err != nil {
return nil, err
}
Expand All @@ -378,7 +380,7 @@ func (b *Builder) getExes(s3Path string) ([]string, error) {

func (b *Builder) prepareAndInstallArtifacts(def *Definition, s3Path,
moduleFileData string, exes []string) error {
imageData, err := b.s3.OpenFile(filepath.Join(s3Path, imageBasename))
imageData, err := b.s3.OpenFile(filepath.Join(s3Path, ImageBasename))
if err != nil {
return err
}
Expand Down Expand Up @@ -408,24 +410,24 @@ func (b *Builder) prepareArtifactsFromS3AndSendToCoreAndS3(def *Definition, s3Pa

return b.addArtifactsToRepo(
map[string]io.Reader{
spackLock: bytes.NewReader(lockData),
softpackYaml: strings.NewReader(concreteSpackYAMLFile),
singularityDefBasename: strings.NewReader(singDef),
builderOut: logData,
SpackLockFile: bytes.NewReader(lockData),
SoftpackYaml: strings.NewReader(concreteSpackYAMLFile),
SingularityDefBasename: strings.NewReader(singDef),
BuilderOut: logData,
moduleForCoreBasename: strings.NewReader(moduleFileData),
usageBasename: strings.NewReader(readme),
UsageBasename: strings.NewReader(readme),
},
def.FullEnvironmentPath(),
)
}

func (b *Builder) getArtifactDataFromS3(s3Path string) (io.Reader, []byte, error) {
logData, err := b.s3.OpenFile(filepath.Join(s3Path, builderOut))
logData, err := b.s3.OpenFile(filepath.Join(s3Path, BuilderOut))
if err != nil {
return nil, nil, err
}

lockFile, err := b.s3.OpenFile(filepath.Join(s3Path, spackLock))
lockFile, err := b.s3.OpenFile(filepath.Join(s3Path, SpackLockFile))
if err != nil {
return nil, nil, err
}
Expand All @@ -446,7 +448,7 @@ func (b *Builder) generateAndUploadSpackYAML(lockData []byte, description string
}

if err = b.s3.UploadData(strings.NewReader(concreteSpackYAMLFile),
filepath.Join(s3Path, softpackYaml)); err != nil {
filepath.Join(s3Path, SoftpackYaml)); err != nil {
return "", err
}

Expand Down Expand Up @@ -520,7 +522,7 @@ func SpackLockToSoftPackYML(spackLockData []byte, desc string, exes []string) (s
func (b *Builder) generateAndUploadUsageFile(def *Definition, s3Path string) (string, error) {
readme := def.ModuleUsage(b.config.Module.LoadPath)

if err := b.s3.UploadData(strings.NewReader(readme), filepath.Join(s3Path, usageBasename)); err != nil {
if err := b.s3.UploadData(strings.NewReader(readme), filepath.Join(s3Path, UsageBasename)); err != nil {
return "", err
}

Expand All @@ -541,15 +543,17 @@ func (b *Builder) addArtifactsToRepo(artifacts map[string]io.Reader, envPath str

defer pw.Close()

req, err := http.NewRequestWithContext(ctx, http.MethodPost, b.config.CoreURL+"?"+url.QueryEscape(envPath), pr)
uploadURL := strings.TrimSuffix(b.config.CoreURL, "/") + uploadEndpoint + "?" + url.QueryEscape(envPath)

req, err := http.NewRequestWithContext(ctx, http.MethodPost, uploadURL, pr)
if err != nil {
return err
}

req.Header.Add("Content-Type", writer.FormDataContentType())

resp, err := http.DefaultClient.Do(req)
slog.Debug("addArtifactsToRepo", "url", b.config.CoreURL+"?"+url.QueryEscape(envPath), "err", err)
slog.Debug("addArtifactsToRepo", "url", b.config.CoreURL+uploadEndpoint+"?"+url.QueryEscape(envPath), "err", err)

if err != nil {
return err
Expand Down
28 changes: 14 additions & 14 deletions build/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,19 +79,19 @@ func (m *mockS3) UploadData(data io.Reader, dest string) error {
}

func (m *mockS3) OpenFile(source string) (io.ReadCloser, error) {
if filepath.Base(source) == exesBasename {
if filepath.Base(source) == ExesBasename {
return io.NopCloser(strings.NewReader(m.exes)), nil
}

if filepath.Base(source) == builderOut {
if filepath.Base(source) == BuilderOut {
return io.NopCloser(strings.NewReader("output")), nil
}

if filepath.Base(source) == spackLock {
if filepath.Base(source) == SpackLockFile {
return io.NopCloser(strings.NewReader(`{"_meta":{"file-type":"spack-lockfile","lockfile-version":5,"specfile-version":4},"spack":{"version":"0.21.0.dev0","type":"git","commit":"dac3b453879439fd733b03d0106cc6fe070f71f6"},"roots":[{"hash":"oibd5a4hphfkgshqiav4fdkvw4hsq4ek","spec":"xxhash arch=None-None-x86_64_v3"}, {"hash":"1ibd5a4hphfkgshqiav4fdkvw4hsq4e1","spec":"py-anndata arch=None-None-x86_64_v3"}, {"hash":"2ibd5a4hphfkgshqiav4fdkvw4hsq4e2","spec":"r-seurat arch=None-None-x86_64_v3"}],"concrete_specs":{"oibd5a4hphfkgshqiav4fdkvw4hsq4ek":{"name":"xxhash","version":"0.8.1","arch":{"platform":"linux","platform_os":"ubuntu22.04","target":"x86_64_v3"},"compiler":{"name":"gcc","version":"11.4.0"},"namespace":"builtin","parameters":{"build_system":"makefile","cflags":[],"cppflags":[],"cxxflags":[],"fflags":[],"ldflags":[],"ldlibs":[]},"package_hash":"wuj5b2kjnmrzhtjszqovcvgc3q46m6hoehmiccimi5fs7nmsw22a====","hash":"oibd5a4hphfkgshqiav4fdkvw4hsq4ek"},"2ibd5a4hphfkgshqiav4fdkvw4hsq4e2":{"name":"r-seurat","version":"4","arch":{"platform":"linux","platform_os":"ubuntu22.04","target":"x86_64_v3"},"compiler":{"name":"gcc","version":"11.4.0"},"namespace":"builtin","parameters":{"build_system":"makefile","cflags":[],"cppflags":[],"cxxflags":[],"fflags":[],"ldflags":[],"ldlibs":[]},"package_hash":"2uj5b2kjnmrzhtjszqovcvgc3q46m6hoehmiccimi5fs7nmsw222====","hash":"2ibd5a4hphfkgshqiav4fdkvw4hsq4e2"}, "1ibd5a4hphfkgshqiav4fdkvw4hsq4e1":{"name":"py-anndata","version":"3.14","arch":{"platform":"linux","platform_os":"ubuntu22.04","target":"x86_64_v3"},"compiler":{"name":"gcc","version":"11.4.0"},"namespace":"builtin","parameters":{"build_system":"makefile","cflags":[],"cppflags":[],"cxxflags":[],"fflags":[],"ldflags":[],"ldlibs":[]},"package_hash":"2uj5b2kjnmrzhtjszqovcvgc3q46m6hoehmiccimi5fs7nmsw222====","hash":"1ibd5a4hphfkgshqiav4fdkvw4hsq4e1"}}}`)), nil //nolint:lll
}

if filepath.Base(source) == imageBasename {
if filepath.Base(source) == ImageBasename {
return io.NopCloser(strings.NewReader("image")), nil
}

Expand Down Expand Up @@ -369,8 +369,8 @@ Stage: final
modulePath := filepath.Join(conf.Module.ModuleInstallDir,
def.EnvironmentPath, def.EnvironmentName, def.EnvironmentVersion)
scriptsPath := filepath.Join(conf.Module.ScriptsInstallDir,
def.EnvironmentPath, def.EnvironmentName, def.EnvironmentVersion+scriptsDirSuffix)
imagePath := filepath.Join(scriptsPath, imageBasename)
def.EnvironmentPath, def.EnvironmentName, def.EnvironmentVersion+ScriptsDirSuffix)
imagePath := filepath.Join(scriptsPath, ImageBasename)
expectedExes := []string{"python", "R", "Rscript", "xxhsum", "xxh32sum", "xxh64sum", "xxh128sum"}

expectedFiles := []string{modulePath, scriptsPath, imagePath}
Expand Down Expand Up @@ -451,19 +451,19 @@ packages:
expectedReadmeContent := "module load " + moduleLoadPrefix + "/groups/hgi/xxhash/0.8.1"

for file, expectedData := range map[string]string{
softpackYaml: expectedSoftpackYaml,
SoftpackYaml: expectedSoftpackYaml,
moduleForCoreBasename: "module-whatis",
singularityDefBasename: "specs:\n - [email protected] arch=None-None-x86_64_v4",
spackLock: `"concrete_specs":`,
builderOut: "output",
usageBasename: expectedReadmeContent,
SingularityDefBasename: "specs:\n - [email protected] arch=None-None-x86_64_v4",
SpackLockFile: `"concrete_specs":`,
BuilderOut: "output",
UsageBasename: expectedReadmeContent,
} {
data, okg := mc.getFile("groups/hgi/xxhash-0.8.1/" + file)
So(okg, ShouldBeTrue)
So(data, ShouldContainSubstring, expectedData)
}

_, ok = mc.getFile("groups/hgi/xxhash-0.8.1/" + imageBasename)
_, ok = mc.getFile("groups/hgi/xxhash-0.8.1/" + ImageBasename)
So(ok, ShouldBeFalse)

So(ms3.softpackYML, ShouldEqual, expectedSoftpackYaml)
Expand Down Expand Up @@ -491,7 +491,7 @@ packages:
So(logWriter.String(), ShouldContainSubstring,
"msg=\"Async part of build failed\" err=\"Mock error\" s3Path=some_path/groups/hgi/xxhash/0.8.1")

data, ok := mc.getFile("groups/hgi/xxhash-0.8.1/" + builderOut)
data, ok := mc.getFile("groups/hgi/xxhash-0.8.1/" + BuilderOut)
So(ok, ShouldBeTrue)
So(data, ShouldContainSubstring, "output")
})
Expand Down Expand Up @@ -535,7 +535,7 @@ packages:
})
So(ok, ShouldBeTrue)

expectedLog := "Post \\\"http://0.0.0.0:1234?groups%2Fhgi%2Fxxhash-0.8.1\\\"" +
expectedLog := "Post \\\"http://0.0.0.0:1234" + uploadEndpoint + "?groups%2Fhgi%2Fxxhash-0.8.1\\\"" +
": dial tcp 0.0.0.0:1234: connect: connection refused"
So(logWriter.String(), ShouldContainSubstring, expectedLog)

Expand Down
29 changes: 21 additions & 8 deletions build/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ import (
)

const (
imageBasename = "singularity.sif"
scriptsDirSuffix = "-scripts"
perms = 0755
flags = os.O_EXCL | os.O_CREATE | os.O_WRONLY
ScriptsDirSuffix = "-scripts"

ImageBasename = "singularity.sif"
perms = 0755
flags = os.O_EXCL | os.O_CREATE | os.O_WRONLY
)

func installModule(scriptInstallBase, moduleInstallBase string, def *Definition, module,
Expand All @@ -58,17 +59,17 @@ func installModule(scriptInstallBase, moduleInstallBase string, def *Definition,
return err
}

if err = installFile(image, filepath.Join(scriptsDir, imageBasename)); err != nil {
if err = installFile(image, filepath.Join(scriptsDir, ImageBasename)); err != nil {
return err
}

return createExeSymlinks(wrapperScript, scriptsDir, exes)
}

func makeModuleDirs(scriptInstallBase, moduleInstallBase string, def *Definition) (string, string, error) {
scriptsDir := filepath.Join(scriptInstallBase, def.EnvironmentPath,
def.EnvironmentName, def.EnvironmentVersion+scriptsDirSuffix)
moduleDir := filepath.Join(moduleInstallBase, def.EnvironmentPath, def.EnvironmentName)
scriptsDir := ScriptsDirFromNameAndVersion(scriptInstallBase, def.EnvironmentPath,
def.EnvironmentName, def.EnvironmentVersion)
moduleDir := ModuleDirFromName(moduleInstallBase, def.EnvironmentPath, def.EnvironmentName)

if err := makeDirectory(scriptsDir, scriptInstallBase); err != nil {
return "", "", err
Expand All @@ -81,6 +82,18 @@ func makeModuleDirs(scriptInstallBase, moduleInstallBase string, def *Definition
return scriptsDir, moduleDir, nil
}

// ScriptsDirFromNameAndVersion returns the canonical scripts path for an
// environment.
func ScriptsDirFromNameAndVersion(scriptInstallBase, path, name, version string) string {
return filepath.Join(scriptInstallBase, path,
name, version+ScriptsDirSuffix)
}

// ModuleDirFromName returns the canonical module path for an environment.
func ModuleDirFromName(moduleInstallBase, path, name string) string {
return filepath.Join(moduleInstallBase, path, name)
}

// makeDirectory does a MkdirAll for leafDir, and then makes sure it and it's
// parents up to baseDir are world accesible.
func makeDirectory(leafDir, baseDir string) error {
Expand Down
4 changes: 2 additions & 2 deletions build/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ func TestInstall(t *testing.T) {
createdModuleFile := readFile(t, filepath.Join(tmpModulesDir, def.EnvironmentPath,
def.EnvironmentName, def.EnvironmentVersion))
scriptsDir := filepath.Join(tmpScriptsDir, def.EnvironmentPath, def.EnvironmentName,
def.EnvironmentVersion+scriptsDirSuffix)
createdImageFile := readFile(t, filepath.Join(scriptsDir, imageBasename))
def.EnvironmentVersion+ScriptsDirSuffix)
createdImageFile := readFile(t, filepath.Join(scriptsDir, ImageBasename))

So(createdModuleFile, ShouldEqual, moduleFile)
So(createdImageFile, ShouldEqual, imageFile)
Expand Down
2 changes: 1 addition & 1 deletion cmd/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ func printIfTTY(msg string) {
return
}

fmt.Println(msg)
cliPrint(msg)
}

func openOrDie(path string) *os.File {
Expand Down
79 changes: 79 additions & 0 deletions cmd/remove.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package cmd

import (
"fmt"
"path/filepath"

"github.com/spf13/cobra"
"github.com/wtsi-hgi/go-softpack-builder/remove"
"github.com/wtsi-hgi/go-softpack-builder/s3"
)

const numArgs = 2

var removeCmd = &cobra.Command{
Use: "remove",
Short: "Remove an environment",
Long: `Remove an environment.

Remove an existing environment; it's entry in the Core artefacts repo, the
module files and the singularity image and symlinks.

Usage: gsb remove softpack/env/path version
`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) < 1 {
die("environment path required")
}

if len(args) < numArgs {
die("environment version required")
}

if len(args) != numArgs {
die("unexpected arguments")
}

conf := getConfig()

s, err := s3.New(conf.S3.BuildBase)
if err != nil {
die(err.Error())
}

envPath := cleanEnvPath(args[0])

if envPath != args[0] {
die("invalid environment path")
}

cliPrint(
"Will now remove environment %s-%s from artefacts repo and modules.\n"+
"Are you sure you sure you wish to proceed? [yN]: ",
envPath,
args[1],
)

var resp string

fmt.Scan(&resp)

if resp != "y" {
return
}

if err := remove.Remove(conf, s, envPath, args[1]); err != nil {
die(err.Error())
}
},
}

func init() {
RootCmd.AddCommand(removeCmd)
}

// cleanEnvPath strips out any attempts to manipulate the envPath in order to
// get GSB to remove incorrect files.
func cleanEnvPath(envPath string) string {
return filepath.Clean(string(filepath.Separator) + envPath)[1:]
}
5 changes: 5 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,8 @@ func die(msg string, a ...interface{}) {
slog.Error(fmt.Sprintf(msg, a...))
os.Exit(1)
}

// cliPrint outputs the message to STDOUT.
func cliPrint(msg string, a ...interface{}) {
fmt.Fprintf(os.Stdout, msg, a...)
}
Loading