From b5a4fe9c986cc8380f907c5b3a1e6c71fd7ce3f6 Mon Sep 17 00:00:00 2001 From: Lionello Lunesu Date: Wed, 26 Jun 2024 18:25:23 -0700 Subject: [PATCH 01/15] do http retries --- src/cmd/cli/command/version.go | 12 +++------ src/cmd/cli/command/version_test.go | 6 ++++- src/go.mod | 2 +- src/pkg/http/client.go | 20 ++++++++++++++ src/pkg/http/get.go | 8 +++--- src/pkg/http/post.go | 3 +-- src/pkg/http/put.go | 14 ++-------- src/pkg/http/put_test.go | 41 +++++++++++++++++++++++------ src/pkg/http/query.go | 12 +++++++++ src/pkg/http/query_test.go | 12 +++++++++ 10 files changed, 93 insertions(+), 37 deletions(-) create mode 100644 src/pkg/http/client.go create mode 100644 src/pkg/http/query.go create mode 100644 src/pkg/http/query_test.go diff --git a/src/cmd/cli/command/version.go b/src/cmd/cli/command/version.go index db6235729..fbca10adc 100644 --- a/src/cmd/cli/command/version.go +++ b/src/cmd/cli/command/version.go @@ -4,14 +4,12 @@ import ( "context" "encoding/json" "errors" - "net/http" "strings" + "github.com/DefangLabs/defang/src/pkg/http" "golang.org/x/mod/semver" ) -var httpClient = http.DefaultClient - func isNewer(current, comparand string) bool { version, ok := normalizeVersion(current) if !ok { @@ -38,16 +36,12 @@ func GetCurrentVersion() string { } func GetLatestVersion(ctx context.Context) (string, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.github.com/repos/DefangLabs/defang/releases/latest", nil) - if err != nil { - return "", err - } - resp, err := httpClient.Do(req) + resp, err := http.GetWithContext(ctx, "https://api.github.com/repos/DefangLabs/defang/releases/latest") if err != nil { return "", err } defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { + if resp.StatusCode != 200 { // The primary rate limit for unauthenticated requests is 60 requests per hour, per IP. return "", errors.New(resp.Status) } diff --git a/src/cmd/cli/command/version_test.go b/src/cmd/cli/command/version_test.go index f072602a7..ae9fbd34f 100644 --- a/src/cmd/cli/command/version_test.go +++ b/src/cmd/cli/command/version_test.go @@ -6,6 +6,8 @@ import ( "net/http" "net/http/httptest" "testing" + + ourHttp "github.com/DefangLabs/defang/src/pkg/http" ) func TestIsNewer(t *testing.T) { @@ -75,7 +77,9 @@ func TestGetLatestVersion(t *testing.T) { rec.Header().Add("Content-Type", "application/json") rec.WriteString(fmt.Sprintf(`{"tag_name":"%v"}`, version)) - httpClient = &http.Client{Transport: &mockRoundTripper{ + client := ourHttp.DefaultClient + t.Cleanup(func() { ourHttp.DefaultClient = client }) + ourHttp.DefaultClient = &http.Client{Transport: &mockRoundTripper{ method: http.MethodGet, url: "https://api.github.com/repos/DefangLabs/defang/releases/latest", resp: rec.Result(), diff --git a/src/go.mod b/src/go.mod index 39bd8accf..2a9a2eebf 100644 --- a/src/go.mod +++ b/src/go.mod @@ -21,6 +21,7 @@ require ( github.com/digitalocean/godo v1.111.0 github.com/docker/docker v25.0.5+incompatible github.com/google/uuid v1.6.0 + github.com/hashicorp/go-retryablehttp v0.7.7 github.com/miekg/dns v1.1.59 github.com/moby/patternmatcher v0.6.0 github.com/muesli/termenv v0.15.2 @@ -43,7 +44,6 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect github.com/rivo/uniseg v0.2.0 // indirect diff --git a/src/pkg/http/client.go b/src/pkg/http/client.go new file mode 100644 index 000000000..0b5362fb1 --- /dev/null +++ b/src/pkg/http/client.go @@ -0,0 +1,20 @@ +package http + +import ( + "github.com/DefangLabs/defang/src/pkg/term" + "github.com/hashicorp/go-retryablehttp" +) + +var DefaultClient = newClient().StandardClient() + +type termLogger struct{} + +func (termLogger) Printf(format string, args ...interface{}) { + term.Debugf(format, args...) +} + +func newClient() *retryablehttp.Client { + c := retryablehttp.NewClient() // default client retries 4 times: 1+2+4+8 = 15s max + c.Logger = termLogger{} + return c +} diff --git a/src/pkg/http/get.go b/src/pkg/http/get.go index 500e26216..b701066b0 100644 --- a/src/pkg/http/get.go +++ b/src/pkg/http/get.go @@ -8,20 +8,20 @@ import ( type Header = http.Header func GetWithContext(ctx context.Context, url string) (*http.Response, error) { - hreq, err := http.NewRequestWithContext(ctx, "GET", url, nil) + hreq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, err } - return http.DefaultClient.Do(hreq) + return DefaultClient.Do(hreq) } func GetWithHeader(ctx context.Context, url string, header http.Header) (*http.Response, error) { - hreq, err := http.NewRequestWithContext(ctx, "GET", url, nil) + hreq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, err } hreq.Header = header - return http.DefaultClient.Do(hreq) + return DefaultClient.Do(hreq) } func GetWithAuth(ctx context.Context, url, auth string) (*http.Response, error) { diff --git a/src/pkg/http/post.go b/src/pkg/http/post.go index 0284f822b..d229df4f4 100644 --- a/src/pkg/http/post.go +++ b/src/pkg/http/post.go @@ -3,13 +3,12 @@ package http import ( "fmt" "io" - "net/http" "net/url" ) // PostForValues issues a POST to the specified URL and returns the response body as url.Values. func PostForValues(_url, contentType string, body io.Reader) (url.Values, error) { - resp, err := http.Post(_url, contentType, body) + resp, err := DefaultClient.Post(_url, contentType, body) if err != nil { return nil, err } diff --git a/src/pkg/http/put.go b/src/pkg/http/put.go index 51aa99475..8304b72b4 100644 --- a/src/pkg/http/put.go +++ b/src/pkg/http/put.go @@ -4,7 +4,6 @@ import ( "context" "io" "net/http" - "net/url" ) // Put issues a PUT to the specified URL. @@ -19,19 +18,10 @@ import ( // See the Client.Do method documentation for details on how redirects // are handled. func Put(ctx context.Context, url string, contentType string, body io.Reader) (*http.Response, error) { - req, err := http.NewRequestWithContext(ctx, "PUT", url, body) + req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, body) if err != nil { return nil, err } req.Header.Set("Content-Type", contentType) - return http.DefaultClient.Do(req) -} - -func RemoveQueryParam(qurl string) string { - u, err := url.Parse(qurl) - if err != nil { - return qurl - } - u.RawQuery = "" - return u.String() + return DefaultClient.Do(req) } diff --git a/src/pkg/http/put_test.go b/src/pkg/http/put_test.go index da56cf021..5787cacf4 100644 --- a/src/pkg/http/put_test.go +++ b/src/pkg/http/put_test.go @@ -1,12 +1,37 @@ package http -import "testing" - -func TestRemoveQueryParam(t *testing.T) { - url := "https://example.com/foo?bar=baz" - expected := "https://example.com/foo" - actual := RemoveQueryParam(url) - if actual != expected { - t.Errorf("expected %q, got %q", expected, actual) +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestPutRetries(t *testing.T) { + const body = "test" + calls := 0 + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + if calls < 3 { + http.Error(w, "error", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + if b, err := io.ReadAll(r.Body); err != nil || string(b) != body { + t.Error("expected body to be read") + } + })) + t.Cleanup(server.Close) + + resp, err := Put(context.Background(), server.URL, "text/plain", strings.NewReader(body)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + defer resp.Body.Close() + if calls != 3 { + t.Errorf("expected 3 calls, got %d", calls) } } diff --git a/src/pkg/http/query.go b/src/pkg/http/query.go new file mode 100644 index 000000000..789c236f3 --- /dev/null +++ b/src/pkg/http/query.go @@ -0,0 +1,12 @@ +package http + +import "net/url" + +func RemoveQueryParam(qurl string) string { + u, err := url.Parse(qurl) + if err != nil { + return qurl + } + u.RawQuery = "" + return u.String() +} diff --git a/src/pkg/http/query_test.go b/src/pkg/http/query_test.go new file mode 100644 index 000000000..da56cf021 --- /dev/null +++ b/src/pkg/http/query_test.go @@ -0,0 +1,12 @@ +package http + +import "testing" + +func TestRemoveQueryParam(t *testing.T) { + url := "https://example.com/foo?bar=baz" + expected := "https://example.com/foo" + actual := RemoveQueryParam(url) + if actual != expected { + t.Errorf("expected %q, got %q", expected, actual) + } +} From 9e9194a8f816927c884e098372c5d4fc8302eedb Mon Sep 17 00:00:00 2001 From: Edward J Date: Wed, 26 Jun 2024 21:46:38 -0700 Subject: [PATCH 02/15] Create sample dir before saving files --- src/pkg/cli/new.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pkg/cli/new.go b/src/pkg/cli/new.go index a1a07c845..272b0919f 100644 --- a/src/pkg/cli/new.go +++ b/src/pkg/cli/new.go @@ -77,6 +77,9 @@ func InitFromSamples(ctx context.Context, dir string, names []string) error { subdir := "" if len(names) > 1 { subdir = name + if err := os.MkdirAll(filepath.Join(dir, subdir), 0755); err != nil { + return err + } } prefix := fmt.Sprintf("%s-%s/samples/%s/", repo, branch, name) if base, ok := strings.CutPrefix(h.Name, prefix); ok && len(base) > 0 { From 1b24817a9c1bf4751f0fb6b7d981a3b5f2bd8255 Mon Sep 17 00:00:00 2001 From: Christopher Yihan Jiang <48700578+Chrisyhjiang@users.noreply.github.com> Date: Thu, 27 Jun 2024 10:27:01 -0700 Subject: [PATCH 03/15] fixed small bug in CLI --- src/pkg/cli/new.go | 25 +++++++++++++++++++------ src/pkg/cli/new_test.go | 16 ++++++++++++++++ 2 files changed, 35 insertions(+), 6 deletions(-) create mode 100644 src/pkg/cli/new_test.go diff --git a/src/pkg/cli/new.go b/src/pkg/cli/new.go index a1a07c845..e86da741a 100644 --- a/src/pkg/cli/new.go +++ b/src/pkg/cli/new.go @@ -63,6 +63,12 @@ func InitFromSamples(ctx context.Context, dir string, names []string) error { defer tarball.Close() tarReader := tar.NewReader(tarball) term.Info("Writing files to disk...") + + found := make(map[string]bool) + for _, name := range names { + found[name] = false + } + for { h, err := tarReader.Next() if err != nil { @@ -73,13 +79,14 @@ func InitFromSamples(ctx context.Context, dir string, names []string) error { } for _, name := range names { - // Create a subdirectory for each sample when there is more than one sample requested - subdir := "" - if len(names) > 1 { - subdir = name - } prefix := fmt.Sprintf("%s-%s/samples/%s/", repo, branch, name) if base, ok := strings.CutPrefix(h.Name, prefix); ok && len(base) > 0 { + found[name] = true + // Create a subdirectory for each sample when there is more than one sample requested + subdir := "" + if len(names) > 1 { + subdir = name + } fmt.Println(" -", base) path := filepath.Join(dir, subdir, base) if h.FileInfo().IsDir() { @@ -94,9 +101,15 @@ func InitFromSamples(ctx context.Context, dir string, names []string) error { } } } + + for _, name := range names { + if !found[name] { + return fmt.Errorf("sample not found") + } + } + return nil } - func createFile(base string, h *tar.Header, tarReader *tar.Reader) error { // Like os.Create, but with the same mode as the original file (so scripts are executable, etc.) file, err := os.OpenFile(base, os.O_RDWR|os.O_CREATE|os.O_EXCL, h.FileInfo().Mode()) diff --git a/src/pkg/cli/new_test.go b/src/pkg/cli/new_test.go new file mode 100644 index 000000000..cc0c834d8 --- /dev/null +++ b/src/pkg/cli/new_test.go @@ -0,0 +1,16 @@ +package cli + +import ( + "context" + "testing" +) + +func TestInitFromSamples(t *testing.T) { + err := InitFromSamples(context.Background(), t.TempDir(), []string{"nonexisting"}) + if err == nil { + t.Fatal("Expected test to fail") + } + if err.Error() != "sample not found" { + t.Error("Expected 'sample not found' error") + } +} From 74f5c349134adc92237fbe4081473aef6af80f1f Mon Sep 17 00:00:00 2001 From: Christopher Yihan Jiang <48700578+Chrisyhjiang@users.noreply.github.com> Date: Thu, 27 Jun 2024 10:32:03 -0700 Subject: [PATCH 04/15] small change --- src/pkg/cli/new.go | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/pkg/cli/new.go b/src/pkg/cli/new.go index ea6d147a8..1982f1098 100644 --- a/src/pkg/cli/new.go +++ b/src/pkg/cli/new.go @@ -64,10 +64,7 @@ func InitFromSamples(ctx context.Context, dir string, names []string) error { tarReader := tar.NewReader(tarball) term.Info("Writing files to disk...") - found := make(map[string]bool) - for _, name := range names { - found[name] = false - } + sampleFound := false for { h, err := tarReader.Next() @@ -89,6 +86,7 @@ func InitFromSamples(ctx context.Context, dir string, names []string) error { } prefix := fmt.Sprintf("%s-%s/samples/%s/", repo, branch, name) if base, ok := strings.CutPrefix(h.Name, prefix); ok && len(base) > 0 { + sampleFound = true fmt.Println(" -", base) path := filepath.Join(dir, subdir, base) if h.FileInfo().IsDir() { @@ -103,13 +101,9 @@ func InitFromSamples(ctx context.Context, dir string, names []string) error { } } } - - for _, name := range names { - if !found[name] { - return fmt.Errorf("sample not found") - } + if !sampleFound { + return fmt.Errorf("sample not found") } - return nil } func createFile(base string, h *tar.Header, tarReader *tar.Reader) error { From 3ea5b83a2c9e4f6880497b5c5d594caeb8441f32 Mon Sep 17 00:00:00 2001 From: Edward J Date: Thu, 27 Jun 2024 11:13:07 -0700 Subject: [PATCH 05/15] Create dir and pass to geneate with AI instead of Chdir --- src/cmd/cli/command/commands.go | 22 ++++++++-------------- src/pkg/cli/generate.go | 8 ++++++-- src/pkg/cli/new.go | 3 +++ 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/cmd/cli/command/commands.go b/src/cmd/cli/command/commands.go index fc988a07f..c9f07b296 100644 --- a/src/cmd/cli/command/commands.go +++ b/src/cmd/cli/command/commands.go @@ -536,30 +536,20 @@ Generate will write files in the current folder. You can edit them and then depl Track("Generate Started", P{"language", language}, P{"sample", sample}, P{"description", prompt.Description}, P{"folder", prompt.Folder}) - // create the folder if needed - cd := "" - if prompt.Folder != "." { - cd = "`cd " + prompt.Folder + "` and " - os.MkdirAll(prompt.Folder, 0755) - if err := os.Chdir(prompt.Folder); err != nil { - return err - } - } - // Check if the current folder is empty - if empty, err := pkg.IsDirEmpty("."); !empty || err != nil { - term.Warn("The folder is not empty. We recommend running this command in an empty folder.") + if empty, err := pkg.IsDirEmpty(prompt.Folder); !empty || err != nil { + term.Warnf("The folder %q is not empty. We recommend running this command in an empty folder.", prompt.Folder) } if sample != "" { term.Info("Fetching sample from the Defang repository...") - err := cli.InitFromSamples(cmd.Context(), "", []string{sample}) + err := cli.InitFromSamples(cmd.Context(), prompt.Folder, []string{sample}) if err != nil { return err } } else { term.Info("Working on it. This may take 1 or 2 minutes...") - _, err := cli.GenerateWithAI(cmd.Context(), client, language, prompt.Description) + _, err := cli.GenerateWithAI(cmd.Context(), client, language, prompt.Folder, prompt.Description) if err != nil { return err } @@ -574,6 +564,10 @@ Generate will write files in the current folder. You can edit them and then depl term.Debug("unable to launch VS Code:", err) } + cd := "" + if prompt.Folder != "." { + cd = "`cd " + prompt.Folder + "` and " + } printDefangHint("Check the files in your favorite editor.\nTo deploy the service, "+cd+"do:", "compose up") return nil }, diff --git a/src/pkg/cli/generate.go b/src/pkg/cli/generate.go index 30609837d..5e41f5b9e 100644 --- a/src/pkg/cli/generate.go +++ b/src/pkg/cli/generate.go @@ -4,13 +4,14 @@ import ( "context" "fmt" "os" + "path/filepath" "github.com/DefangLabs/defang/src/pkg/cli/client" "github.com/DefangLabs/defang/src/pkg/term" defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1" ) -func GenerateWithAI(ctx context.Context, client client.Client, language string, description string) ([]string, error) { +func GenerateWithAI(ctx context.Context, client client.Client, language, dir, description string) ([]string, error) { if DoDryRun { term.Warn("Dry run, not generating files") return nil, ErrDryRun @@ -38,11 +39,14 @@ func GenerateWithAI(ctx context.Context, client client.Client, language string, // Write each file to disk term.Info("Writing files to disk...") + if err := os.MkdirAll(dir, 0755); err != nil { + return nil, err + } for _, file := range response.Files { // Print the files that were generated fmt.Println(" -", file.Name) // TODO: this will overwrite existing files - if err = os.WriteFile(file.Name, []byte(file.Content), 0644); err != nil { + if err = os.WriteFile(filepath.Join(dir, file.Name), []byte(file.Content), 0644); err != nil { return nil, err } } diff --git a/src/pkg/cli/new.go b/src/pkg/cli/new.go index 272b0919f..56d2503ec 100644 --- a/src/pkg/cli/new.go +++ b/src/pkg/cli/new.go @@ -63,6 +63,9 @@ func InitFromSamples(ctx context.Context, dir string, names []string) error { defer tarball.Close() tarReader := tar.NewReader(tarball) term.Info("Writing files to disk...") + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } for { h, err := tarReader.Next() if err != nil { From ff871f2119e3c5754ba827ba945e718f5f95b988 Mon Sep 17 00:00:00 2001 From: Edward J Date: Thu, 27 Jun 2024 12:50:02 -0700 Subject: [PATCH 06/15] Only show folder not empty error when folder exist and not empty --- src/cmd/cli/command/commands.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cmd/cli/command/commands.go b/src/cmd/cli/command/commands.go index c9f07b296..4d9c85ed7 100644 --- a/src/cmd/cli/command/commands.go +++ b/src/cmd/cli/command/commands.go @@ -537,7 +537,7 @@ Generate will write files in the current folder. You can edit them and then depl Track("Generate Started", P{"language", language}, P{"sample", sample}, P{"description", prompt.Description}, P{"folder", prompt.Folder}) // Check if the current folder is empty - if empty, err := pkg.IsDirEmpty(prompt.Folder); !empty || err != nil { + if empty, err := pkg.IsDirEmpty(prompt.Folder); !os.IsNotExist(err) && !empty { term.Warnf("The folder %q is not empty. We recommend running this command in an empty folder.", prompt.Folder) } From a57cd49df8576dfb2ca09e8044c78caccebcbaef Mon Sep 17 00:00:00 2001 From: Christopher Yihan Jiang <48700578+Chrisyhjiang@users.noreply.github.com> Date: Thu, 27 Jun 2024 12:53:18 -0700 Subject: [PATCH 07/15] made sure an error is returned before prompt --- src/cmd/cli/command/commands.go | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/cmd/cli/command/commands.go b/src/cmd/cli/command/commands.go index fc988a07f..e5d9fa8f9 100644 --- a/src/cmd/cli/command/commands.go +++ b/src/cmd/cli/command/commands.go @@ -438,7 +438,7 @@ var generateCmd = &cobra.Command{ } return cli.InitFromSamples(cmd.Context(), "", []string{sample}) } - + sampleList, cachedErr := cli.FetchSamples(cmd.Context()) if sample == "" { if err := survey.AskOne(&survey.Select{ Message: "Choose the language you'd like to use:", @@ -448,17 +448,16 @@ var generateCmd = &cobra.Command{ }, &language); err != nil { return err } - // Fetch the list of samples from the Defang repository - if samples, err := cli.FetchSamples(cmd.Context()); err != nil { - term.Debug("unable to fetch samples:", err) - } else if len(samples) > 0 { + if cachedErr != nil { + term.Debug("unable to fetch samples:", cachedErr) + } else if len(sampleList) > 0 { const generateWithAI = "Generate with AI" lang := strings.ToLower(language) sampleNames := []string{generateWithAI} sampleDescriptions := []string{"Generate a sample from scratch using a language prompt"} - for _, sample := range samples { + for _, sample := range sampleList { if slices.Contains(sample.Languages, lang) { sampleNames = append(sampleNames, sample.Name) sampleDescriptions = append(sampleDescriptions, sample.ShortDescription) @@ -510,6 +509,17 @@ Generate will write files in the current folder. You can edit them and then depl if sample != "" { qs = qs[1:] // user picked a sample, so we skip the description question + sampleExists := false + for _, s := range sampleList { + if s.Name == sample { + sampleExists = true + break + } + } + + if !sampleExists { + return errors.New("sample not found") + } } prompt := struct { From e4a236179104f17ea326ad95aeb2dd7569be8d3f Mon Sep 17 00:00:00 2001 From: Christopher Yihan Jiang <48700578+Chrisyhjiang@users.noreply.github.com> Date: Thu, 27 Jun 2024 13:22:41 -0700 Subject: [PATCH 08/15] resolved comments --- src/cmd/cli/command/commands.go | 12 ++++-------- src/pkg/cli/new.go | 5 ++++- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/cmd/cli/command/commands.go b/src/cmd/cli/command/commands.go index e5d9fa8f9..016b4a320 100644 --- a/src/cmd/cli/command/commands.go +++ b/src/cmd/cli/command/commands.go @@ -509,16 +509,12 @@ Generate will write files in the current folder. You can edit them and then depl if sample != "" { qs = qs[1:] // user picked a sample, so we skip the description question - sampleExists := false - for _, s := range sampleList { - if s.Name == sample { - sampleExists = true - break - } - } + sampleExists := slices.ContainsFunc(sampleList, func(s cli.Sample) bool { + return s.Name == sample + }) if !sampleExists { - return errors.New("sample not found") + return cli.ErrSampleNotFound } } diff --git a/src/pkg/cli/new.go b/src/pkg/cli/new.go index 1982f1098..2cbf13725 100644 --- a/src/pkg/cli/new.go +++ b/src/pkg/cli/new.go @@ -5,6 +5,7 @@ import ( "compress/gzip" "context" "encoding/json" + "errors" "fmt" "io" "os" @@ -15,6 +16,8 @@ import ( "github.com/DefangLabs/defang/src/pkg/term" ) +var ErrSampleNotFound = errors.New("sample not found") + type Sample struct { Name string `json:"name"` Title string `json:"title"` @@ -102,7 +105,7 @@ func InitFromSamples(ctx context.Context, dir string, names []string) error { } } if !sampleFound { - return fmt.Errorf("sample not found") + return ErrSampleNotFound } return nil } From c2a54b593193626a4291723f2e0250f47c3d3391 Mon Sep 17 00:00:00 2001 From: Christopher Yihan Jiang <48700578+Chrisyhjiang@users.noreply.github.com> Date: Thu, 27 Jun 2024 13:25:11 -0700 Subject: [PATCH 09/15] rename --- src/cmd/cli/command/commands.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cmd/cli/command/commands.go b/src/cmd/cli/command/commands.go index 016b4a320..b8bbf8815 100644 --- a/src/cmd/cli/command/commands.go +++ b/src/cmd/cli/command/commands.go @@ -438,7 +438,7 @@ var generateCmd = &cobra.Command{ } return cli.InitFromSamples(cmd.Context(), "", []string{sample}) } - sampleList, cachedErr := cli.FetchSamples(cmd.Context()) + sampleList, fetchSamplesErr := cli.FetchSamples(cmd.Context()) if sample == "" { if err := survey.AskOne(&survey.Select{ Message: "Choose the language you'd like to use:", @@ -449,8 +449,8 @@ var generateCmd = &cobra.Command{ return err } // Fetch the list of samples from the Defang repository - if cachedErr != nil { - term.Debug("unable to fetch samples:", cachedErr) + if fetchSamplesErr != nil { + term.Debug("unable to fetch samples:", fetchSamplesErr) } else if len(sampleList) > 0 { const generateWithAI = "Generate with AI" From d8a1d722a9b41db4da77a2fddd913e70bff45085 Mon Sep 17 00:00:00 2001 From: Christopher Yihan Jiang <48700578+Chrisyhjiang@users.noreply.github.com> Date: Thu, 27 Jun 2024 13:37:46 -0700 Subject: [PATCH 10/15] improved testing --- src/pkg/cli/new_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pkg/cli/new_test.go b/src/pkg/cli/new_test.go index cc0c834d8..dcc545ab8 100644 --- a/src/pkg/cli/new_test.go +++ b/src/pkg/cli/new_test.go @@ -2,6 +2,7 @@ package cli import ( "context" + "errors" "testing" ) @@ -10,7 +11,7 @@ func TestInitFromSamples(t *testing.T) { if err == nil { t.Fatal("Expected test to fail") } - if err.Error() != "sample not found" { - t.Error("Expected 'sample not found' error") + if !errors.Is(err, ErrSampleNotFound) { + t.Errorf("Expected error to be %v, got %v", ErrSampleNotFound, err) } } From b17fc512bd489a8dc43150a66c961afd95d75223 Mon Sep 17 00:00:00 2001 From: Christopher Yihan Jiang <48700578+Chrisyhjiang@users.noreply.github.com> Date: Thu, 27 Jun 2024 13:41:06 -0700 Subject: [PATCH 11/15] removed unnecessary line --- src/cmd/cli/command/commands.go | 1 - 1 file changed, 1 deletion(-) diff --git a/src/cmd/cli/command/commands.go b/src/cmd/cli/command/commands.go index b8bbf8815..9b5cf01f0 100644 --- a/src/cmd/cli/command/commands.go +++ b/src/cmd/cli/command/commands.go @@ -99,7 +99,6 @@ func Execute(ctx context.Context) error { if code == connect.CodeFailedPrecondition && (strings.Contains(err.Error(), "EULA") || strings.Contains(err.Error(), "terms")) { printDefangHint("Please use the following command to see the Defang terms of service:", "terms") } - return ExitCode(code) } From 37a5a51bf45ed9cf2de5543c04dbb5a5ffb75108 Mon Sep 17 00:00:00 2001 From: Edward J Date: Thu, 27 Jun 2024 16:23:12 -0700 Subject: [PATCH 12/15] Add configs to Sample struct --- src/pkg/cli/new.go | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pkg/cli/new.go b/src/pkg/cli/new.go index 56d2503ec..533b17264 100644 --- a/src/pkg/cli/new.go +++ b/src/pkg/cli/new.go @@ -24,6 +24,7 @@ type Sample struct { ShortDescription string `json:"shortDescription"` Tags []string `json:"tags"` Languages []string `json:"languages"` + Configs []string `json:"configs"` } func FetchSamples(ctx context.Context) ([]Sample, error) { From 1384b4288a335be0edf7ba39d08eb4f422510ce3 Mon Sep 17 00:00:00 2001 From: Christopher Yihan Jiang <48700578+Chrisyhjiang@users.noreply.github.com> Date: Fri, 28 Jun 2024 09:13:01 -0700 Subject: [PATCH 13/15] add back new line --- src/pkg/cli/new.go | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pkg/cli/new.go b/src/pkg/cli/new.go index 2cbf13725..6fa626669 100644 --- a/src/pkg/cli/new.go +++ b/src/pkg/cli/new.go @@ -109,6 +109,7 @@ func InitFromSamples(ctx context.Context, dir string, names []string) error { } return nil } + func createFile(base string, h *tar.Header, tarReader *tar.Reader) error { // Like os.Create, but with the same mode as the original file (so scripts are executable, etc.) file, err := os.OpenFile(base, os.O_RDWR|os.O_CREATE|os.O_EXCL, h.FileInfo().Mode()) From 73e5b611cd49a0f77251fc0caa3323291d310622 Mon Sep 17 00:00:00 2001 From: Edward J Date: Fri, 28 Jun 2024 09:39:27 -0700 Subject: [PATCH 14/15] Remove a redundant MkdirAll call --- src/pkg/cli/new.go | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/pkg/cli/new.go b/src/pkg/cli/new.go index 533b17264..c18eb22c5 100644 --- a/src/pkg/cli/new.go +++ b/src/pkg/cli/new.go @@ -64,9 +64,6 @@ func InitFromSamples(ctx context.Context, dir string, names []string) error { defer tarball.Close() tarReader := tar.NewReader(tarball) term.Info("Writing files to disk...") - if err := os.MkdirAll(dir, 0755); err != nil { - return err - } for { h, err := tarReader.Next() if err != nil { @@ -77,13 +74,13 @@ func InitFromSamples(ctx context.Context, dir string, names []string) error { } for _, name := range names { - // Create a subdirectory for each sample when there is more than one sample requested + // Create the sample directory or subdirectory for each sample when there is more than one sample requested subdir := "" if len(names) > 1 { subdir = name - if err := os.MkdirAll(filepath.Join(dir, subdir), 0755); err != nil { - return err - } + } + if err := os.MkdirAll(filepath.Join(dir, subdir), 0755); err != nil { + return err } prefix := fmt.Sprintf("%s-%s/samples/%s/", repo, branch, name) if base, ok := strings.CutPrefix(h.Name, prefix); ok && len(base) > 0 { From bb1a191881690da0ad519d28b49175cc905aa7fc Mon Sep 17 00:00:00 2001 From: Lionello Lunesu Date: Fri, 28 Jun 2024 14:14:23 -0700 Subject: [PATCH 15/15] update windows short link post release --- .github/workflows/go.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index bc6e0b49f..085c43bcb 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -178,6 +178,19 @@ jobs: working-directory: ./pkgs/npm steps: + - name: Update Windows s.defang.io/defang_win_amd64.zip short link + run: | + curl --request POST \ + --url https://api.short.io/links/$DEFANG_WIN_AMD64_LNK \ + --header "Authorization: $SHORTIO_PK" \ + --header 'accept: application/json' \ + --header 'content-type: application/json' \ + --data "{\"originalURL\":\"https://github.com/DefangLabs/defang/releases/download/${TAG}/defang_${TAG#v}_windows_amd64.zip\"}" + env: + SHORTIO_PK: ${{ secrets.SHORTIO_PK }} + TAG: ${{ github.ref_name }} + DEFANG_WIN_AMD64_LNK: "lnk_4vSQ_CDukZ5POEE4o0mMDysr2U" + - name: Trigger CLI Autodoc uses: peter-evans/repository-dispatch@v3 with: