Skip to content

Commit 24abf27

Browse files
committed
Implement rv release flow
1 parent 9cb4229 commit 24abf27

File tree

7 files changed

+280
-12
lines changed

7 files changed

+280
-12
lines changed

cmd/common_test.go

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package cmd
2+
3+
import (
4+
"archive/zip"
5+
"bytes"
6+
"errors"
7+
"fmt"
8+
"os"
9+
"regexp"
10+
11+
"github.com/google/uuid"
12+
"github.com/spf13/cobra"
13+
)
14+
15+
// ================
16+
// HELPER FUNCTIONS
17+
// ================
18+
func createRelease(workspacePath, includedFile string) (string, error) {
19+
// create bundle
20+
bundlePath := fmt.Sprintf("%s.zip", uuid.NewString())
21+
defer deleteBundle(bundlePath)
22+
if err := createBundle(bundlePath, includedFile); err != nil {
23+
return "", err
24+
}
25+
26+
// prepare command
27+
cmdOutput := createOutputBuffer(RootCmd)
28+
RootCmd.SetArgs([]string{"release", "-w", workspacePath, bundlePath})
29+
// FIRE!
30+
RootCmd.Execute()
31+
32+
// get release ID from command output
33+
return parseReleaseId(cmdOutput.String()), nil
34+
}
35+
36+
func createOutputBuffer(cmd *cobra.Command) *bytes.Buffer {
37+
actual := new(bytes.Buffer)
38+
cmd.SetOut(actual)
39+
cmd.SetErr(actual)
40+
return actual
41+
}
42+
43+
func parseReleaseId(cmdOutput string) string {
44+
pattern := `\b\d{14}\.\d{3}\b`
45+
// Compile the regular expression
46+
re := regexp.MustCompile(pattern)
47+
return re.FindString(cmdOutput)
48+
}
49+
50+
func fileExists(filename string) bool {
51+
_, err := os.Stat(filename)
52+
return !os.IsNotExist(err)
53+
}
54+
55+
func createBundle(zipFileName, includedFileName string) error {
56+
outFile, err := os.Create(zipFileName)
57+
if err != nil {
58+
return err
59+
}
60+
61+
w := zip.NewWriter(outFile)
62+
63+
if _, err := w.Create(includedFileName); err != nil {
64+
return fmt.Errorf("failed to include %s to zip file", zipFileName)
65+
}
66+
67+
if err := w.Close(); err != nil {
68+
_ = outFile.Close()
69+
return errors.New("Warning: closing zipfile writer failed: " + err.Error())
70+
}
71+
72+
if err := outFile.Close(); err != nil {
73+
return errors.New("Warning: closing zipfile failed: " + err.Error())
74+
}
75+
76+
return nil
77+
}
78+
79+
func deleteBundle(fname string) error {
80+
return os.Remove(fname)
81+
}

cmd/release.go

+8-4
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,28 @@ package cmd
33
import (
44
"fmt"
55

6+
"github.com/kkentzo/rv/release"
67
"github.com/spf13/cobra"
78
)
89

910
// releaseCmd represents the release command
1011
var (
11-
releaseCmdDescription = "Uncompress the specified bundle into the release folder and update the `current` link"
12+
releaseCmdDescription = "Uncompress the specified bundle into the workspace and update the `current` link"
1213
releaseCmd = &cobra.Command{
1314
Use: "release",
1415
Short: releaseCmdDescription,
1516
Long: releaseCmdDescription,
1617
Args: cobra.ExactArgs(1),
1718
Run: func(cmd *cobra.Command, args []string) {
18-
bundlePath := args[0]
19-
fmt.Printf("release command called with bundle_path=%s [workspace=%s]", bundlePath, rootDirectory)
19+
if releaseID, err := release.Install(workspacePath, args[0]); err != nil {
20+
fmt.Fprintf(cmd.OutOrStderr(), "error: %v\n", err)
21+
} else {
22+
fmt.Fprintln(cmd.OutOrStdout(), releaseID)
23+
}
2024
},
2125
}
2226
)
2327

2428
func init() {
25-
rootCmd.AddCommand(releaseCmd)
29+
RootCmd.AddCommand(releaseCmd)
2630
}

cmd/release_test.go

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"io/ioutil"
6+
"os"
7+
"path"
8+
"testing"
9+
"time"
10+
11+
"github.com/google/uuid"
12+
"github.com/stretchr/testify/assert"
13+
)
14+
15+
func Test_Release_ShouldCreateResources_FromScratch(t *testing.T) {
16+
// do not create the workspace -- just specify the path
17+
workspacePath := uuid.NewString()
18+
// ensure that we'll clean up
19+
defer os.RemoveAll(workspacePath)
20+
21+
// create and execute release
22+
releaseId, err := createRelease(workspacePath, "foo.txt")
23+
assert.NoError(t, err)
24+
25+
// the workspace should now be present
26+
assert.DirExists(t, workspacePath)
27+
// the workspace should contain the release (extracted bundle under a versioned directory)
28+
assert.DirExists(t, path.Join(workspacePath, releaseId))
29+
// the release should have the correct contents
30+
assert.FileExists(t, path.Join(workspacePath, releaseId, "foo.txt"))
31+
// the workspace should contain the "current" release link
32+
assert.FileExists(t, path.Join(workspacePath, "current"))
33+
// the "current" release link should point to the release folder
34+
assert.FileExists(t, path.Join(workspacePath, "current", "foo.txt"))
35+
}
36+
37+
func Test_Release_ShouldCleanUp_WhenBundleDoesNotExist(t *testing.T) {
38+
workspacePath := uuid.NewString()
39+
// ensure that we'll clean up
40+
defer os.RemoveAll(workspacePath)
41+
42+
// DO NOT create this bundle
43+
bundlePath := fmt.Sprintf("%s.zip", uuid.NewString())
44+
45+
// prepare and execute command
46+
RootCmd.SetArgs([]string{"release", "-w", workspacePath, bundlePath})
47+
// FIRE!
48+
RootCmd.Execute()
49+
50+
// the workspace will not be cleared up
51+
assert.DirExists(t, workspacePath)
52+
// the workspace directory should be empty
53+
entries, err := ioutil.ReadDir(workspacePath)
54+
assert.NoError(t, err)
55+
assert.Empty(t, entries)
56+
// the workspace should NOT contain the "current" release link
57+
assert.NoFileExists(t, path.Join(workspacePath, "current"))
58+
}
59+
60+
func Test_Release_ShouldUpdateCurrent_WhenPreviousReleaseExists(t *testing.T) {
61+
workspacePath := uuid.NewString()
62+
defer os.RemoveAll(workspacePath)
63+
64+
// === create the first release ===
65+
// create and execute release
66+
releaseId, err := createRelease(workspacePath, "foo.txt")
67+
assert.NoError(t, err)
68+
assert.FileExists(t, path.Join(workspacePath, releaseId, "foo.txt"))
69+
assert.FileExists(t, path.Join(workspacePath, "current", "foo.txt"))
70+
71+
time.Sleep(10 * time.Millisecond)
72+
73+
// === create the second release ===
74+
releaseId, err = createRelease(workspacePath, "bar.txt")
75+
assert.NoError(t, err)
76+
assert.FileExists(t, path.Join(workspacePath, releaseId, "bar.txt"))
77+
assert.FileExists(t, path.Join(workspacePath, "current", "bar.txt"))
78+
}

cmd/root.go

+7-7
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import (
88

99
var (
1010
// persistent (global) command-line arguments
11-
rootDirectory string
11+
workspacePath string
1212
// command
13-
rootCmdDescription = "Manage local releases"
14-
rootCmd = &cobra.Command{
13+
rootCmdDescription = "Manage multiple release bundles locally"
14+
RootCmd = &cobra.Command{
1515
Use: "rv",
1616
Short: rootCmdDescription,
1717
Long: rootCmdDescription,
@@ -21,14 +21,14 @@ var (
2121
// Execute adds all child commands to the root command and sets flags appropriately.
2222
// This is called by main.main(). It only needs to happen once to the rootCmd.
2323
func Execute() {
24-
err := rootCmd.Execute()
24+
err := RootCmd.Execute()
2525
if err != nil {
2626
os.Exit(1)
2727
}
2828
}
2929

3030
func init() {
31-
rootCmd.PersistentFlags().
32-
StringVarP(&rootDirectory, "workspace", "w", "", "directory under which releases will be located and managed")
33-
rootCmd.MarkPersistentFlagRequired("workspace")
31+
RootCmd.PersistentFlags().
32+
StringVarP(&workspacePath, "workspace", "w", "", "directory that contains all available releases")
33+
RootCmd.MarkPersistentFlagRequired("workspace")
3434
}

go.mod

+8-1
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,16 @@ module github.com/kkentzo/rv
22

33
go 1.20
44

5-
require github.com/spf13/cobra v1.8.0
5+
require (
6+
github.com/google/uuid v1.6.0
7+
github.com/spf13/cobra v1.8.0
8+
github.com/stretchr/testify v1.9.0
9+
)
610

711
require (
12+
github.com/davecgh/go-spew v1.1.1 // indirect
813
github.com/inconshreveable/mousetrap v1.1.0 // indirect
14+
github.com/pmezard/go-difflib v1.0.0 // indirect
915
github.com/spf13/pflag v1.0.5 // indirect
16+
gopkg.in/yaml.v3 v3.0.1 // indirect
1017
)

go.sum

+10
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
2+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
5+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
26
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
37
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
8+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
9+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
410
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
511
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
612
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
713
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
814
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
15+
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
16+
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
17+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
918
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
19+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
1020
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

release/release.go

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package release
2+
3+
import (
4+
"archive/zip"
5+
"fmt"
6+
"io"
7+
"os"
8+
"path"
9+
"path/filepath"
10+
"time"
11+
)
12+
13+
func Install(workspaceDir, bundlePath string) (string, error) {
14+
// create release under workspace
15+
id := time.Now().Format("20060102150405.000")
16+
releaseDir := path.Join(workspaceDir, id)
17+
if err := os.MkdirAll(releaseDir, 0755); err != nil {
18+
return id, fmt.Errorf("failed to create release: %v", err)
19+
}
20+
// decompress bundle file
21+
if err := decompressZip(bundlePath, releaseDir); err != nil {
22+
// cleanup release directory
23+
defer os.RemoveAll(releaseDir)
24+
return id, fmt.Errorf("failed to decompress archive: %v", err)
25+
}
26+
// update current link
27+
if err := createOrUpdateLink(workspaceDir, id); err != nil {
28+
// cleanup release directory
29+
defer os.RemoveAll(releaseDir)
30+
return id, fmt.Errorf("failed to create/update link: %v", err)
31+
}
32+
return id, nil
33+
}
34+
35+
func createOrUpdateLink(workspaceDir, target string) error {
36+
link := path.Join(workspaceDir, "current")
37+
// does the link already exist?
38+
_, err := os.Stat(link)
39+
if !os.IsNotExist(err) {
40+
os.Remove(link)
41+
}
42+
return os.Symlink(target, link)
43+
}
44+
45+
func decompressZip(zipFile, targetDir string) error {
46+
// Open the zip archive for reading
47+
r, err := zip.OpenReader(zipFile)
48+
if err != nil {
49+
return err
50+
}
51+
defer r.Close()
52+
53+
// Create the target directory if it doesn't exist
54+
if err := os.MkdirAll(targetDir, 0755); err != nil {
55+
return err
56+
}
57+
58+
// Iterate through each file in the archive
59+
for _, file := range r.File {
60+
// Open the file inside the zip archive
61+
rc, err := file.Open()
62+
if err != nil {
63+
return err
64+
}
65+
defer rc.Close()
66+
67+
// Create the corresponding file in the target directory
68+
targetFilePath := filepath.Join(targetDir, file.Name)
69+
if file.FileInfo().IsDir() {
70+
// Create directories if file is a directory
71+
os.MkdirAll(targetFilePath, file.Mode())
72+
} else {
73+
// Create the file if it doesn't exist
74+
targetFile, err := os.OpenFile(targetFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
75+
if err != nil {
76+
return err
77+
}
78+
defer targetFile.Close()
79+
80+
// Copy contents from the file inside the zip archive to the target file
81+
if _, err := io.Copy(targetFile, rc); err != nil {
82+
return err
83+
}
84+
}
85+
}
86+
87+
return nil
88+
}

0 commit comments

Comments
 (0)