-
Notifications
You must be signed in to change notification settings - Fork 31
feat(experiment): create and delete developer sandboxes #389
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
Changes from 5 commits
c0b11c5
eabc9ba
cea2bcd
b1699bb
a35fbc3
a50fb40
0b525a5
2e50c5b
ca46e13
43e9bac
9851ac7
bf34f27
b1b57a7
6bd1ac5
521833b
f6b4d21
ee75a8e
1b65aa8
9b6634f
74248b8
78376a6
4d7894c
b5de063
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,211 @@ | ||
| // Copyright 2022-2026 Salesforce, Inc. | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| package sandbox | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "strconv" | ||
| "strings" | ||
| "time" | ||
|
|
||
| "github.com/slackapi/slack-cli/internal/shared" | ||
| "github.com/slackapi/slack-cli/internal/slackerror" | ||
| "github.com/slackapi/slack-cli/internal/style" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
|
|
||
| type createFlags struct { | ||
| name string | ||
| domain string | ||
| password string | ||
| locale string | ||
| owningOrgID string | ||
| template string | ||
| eventCode string | ||
| archiveTTL string // TTL duration, e.g. 1d, 2h | ||
| archiveDate string // explicit date yyyy-mm-dd | ||
| } | ||
|
|
||
| var createCmdFlags createFlags | ||
|
|
||
| func NewCreateCommand(clients *shared.ClientFactory) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "create [flags]", | ||
| Short: "Create a developer sandbox", | ||
| Long: `Create a new Slack developer sandbox`, | ||
| Example: style.ExampleCommandsf([]style.ExampleCommand{ | ||
| {Command: "sandbox create --name test-box --password mypass", Meaning: "Create a sandbox named test-box"}, | ||
| {Command: "sandbox create --name test-box --password mypass --domain test-box --archive-ttl 1d", Meaning: "Create a temporary sandbox that will be archived in 1 day"}, | ||
| {Command: "sandbox create --name test-box --password mypass --domain test-box --archive-date 2025-12-31", Meaning: "Create a sandbox that will be archived on a specific date"}, | ||
| }), | ||
| Args: cobra.NoArgs, | ||
| PreRunE: func(cmd *cobra.Command, args []string) error { | ||
| return requireSandboxExperiment(clients) | ||
| }, | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| return runCreateCommand(cmd, clients) | ||
| }, | ||
| } | ||
|
|
||
| cmd.Flags().StringVar(&createCmdFlags.name, "name", "", "Organization name for the new sandbox") | ||
| cmd.Flags().StringVar(&createCmdFlags.domain, "domain", "", "Team domain (e.g., pizzaknifefight). If not provided, derived from org name") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🍕 praise: Toward common workplace shenanigan IIRC!
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I changed this line to just |
||
| cmd.Flags().StringVar(&createCmdFlags.password, "password", "", "Password used to log into the sandbox") | ||
| cmd.Flags().StringVar(&createCmdFlags.locale, "locale", "", "Locale (eg. en-us, languageCode-countryCode)") | ||
| cmd.Flags().StringVar(&createCmdFlags.template, "template", "", "Template ID for pre-defined data to preload") | ||
| cmd.Flags().StringVar(&createCmdFlags.eventCode, "event-code", "", "Event code for the sandbox") | ||
| cmd.Flags().StringVar(&createCmdFlags.archiveTTL, "archive-ttl", "", "Time-to-live duration; sandbox will be archived at end of day after this period (e.g., 2h, 1d, 7d)") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: In the Long Description can be list all of the available formats for TTL?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point; I also updated the parsing of archiveTTL to only recognize day/week/month; no 'hour' option (though we'll likely look at adding support for these shorter archive times in the future) |
||
| cmd.Flags().StringVar(&createCmdFlags.archiveDate, "archive-date", "", "Explicit archive date in yyyy-mm-dd format. Cannot be used with --archive") | ||
|
vegeris marked this conversation as resolved.
Outdated
|
||
|
|
||
| // If one's developer account is managed by multiple Production Slack teams, one of those team IDs must be provided in the command | ||
| cmd.Flags().StringVar(&createCmdFlags.owningOrgID, "owning-org-id", "", "Enterprise team ID that manages your developer account, if applicable") | ||
|
|
||
| if err := cmd.MarkFlagRequired("name"); err != nil { | ||
| panic(err) | ||
|
vegeris marked this conversation as resolved.
Outdated
|
||
| } | ||
| if err := cmd.MarkFlagRequired("password"); err != nil { | ||
| panic(err) | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🏁 issue(non-blocking): We might want prompt alternatives for required flags but no blocker for this PR!
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I went ahead and added those prompts 👀 |
||
|
|
||
| return cmd | ||
| } | ||
|
|
||
| func runCreateCommand(cmd *cobra.Command, clients *shared.ClientFactory) error { | ||
| ctx := cmd.Context() | ||
|
|
||
| auth, err := getSandboxAuth(ctx, clients) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| domain := createCmdFlags.domain | ||
| if domain == "" { | ||
| domain = domainFromName(createCmdFlags.name) | ||
| } | ||
|
|
||
| if createCmdFlags.archiveTTL != "" && createCmdFlags.archiveDate != "" { | ||
| return slackerror.New(slackerror.ErrInvalidArguments). | ||
| WithMessage("Cannot use both --archive-ttl and --archive-date"). | ||
| WithRemediation("Use only one: --archive-ttl for TTL (e.g., 3d) or --archive-date for a specific date (yyyy-mm-dd)") | ||
| } | ||
|
|
||
| archiveEpochDatetime := int64(0) | ||
| if createCmdFlags.archiveTTL != "" { | ||
| archiveEpochDatetime, err = getEpochFromTTL(createCmdFlags.archiveTTL) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } else if createCmdFlags.archiveDate != "" { | ||
| archiveEpochDatetime, err = getEpochFromDate(createCmdFlags.archiveDate) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
|
|
||
| teamID, sandboxURL, err := clients.API().CreateSandbox(ctx, auth.Token, | ||
| createCmdFlags.name, | ||
| domain, | ||
| createCmdFlags.password, | ||
| createCmdFlags.locale, | ||
| createCmdFlags.owningOrgID, | ||
| createCmdFlags.template, | ||
| createCmdFlags.eventCode, | ||
| archiveEpochDatetime, | ||
| ) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| printCreateSuccess(cmd, clients, teamID, sandboxURL) | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // getEpochFromTTL parses a time-to-live string (e.g., "24h", "1d", "7d") and returns the Unix epoch | ||
| // when the sandbox will be archived. Supports Go duration format (h, m, s) and "Nd" for days. | ||
| // The value cannot exceed 6 months. | ||
| func getEpochFromTTL(ttl string) (int64, error) { | ||
| var d time.Duration | ||
| if strings.HasSuffix(strings.ToLower(ttl), "d") { | ||
| numStr := strings.TrimSuffix(strings.ToLower(ttl), "d") | ||
| n, err := strconv.Atoi(numStr) | ||
| if err != nil { | ||
| return 0, slackerror.New(slackerror.ErrInvalidArguments). | ||
| WithMessage("Invalid TTL: %q", ttl). | ||
| WithRemediation("Use a duration like 2h, 1d, or 7d") | ||
| } | ||
| d = time.Duration(n) * 24 * time.Hour | ||
| } else { | ||
| var err error | ||
| d, err = time.ParseDuration(ttl) | ||
| if err != nil { | ||
| return 0, slackerror.New(slackerror.ErrInvalidArguments). | ||
| WithMessage("Invalid TTL: %q", ttl). | ||
| WithRemediation("Use a duration like 2h, 1d, or 7d") | ||
| } | ||
| } | ||
| return time.Now().Add(d).Unix(), nil | ||
| } | ||
|
|
||
| // getEpochFromDate parses a date in yyyy-mm-dd format and returns the Unix epoch at start of that day (UTC). | ||
| func getEpochFromDate(dateStr string) (int64, error) { | ||
| dateFormat := "2006-01-02" | ||
| t, err := time.ParseInLocation(dateFormat, dateStr, time.UTC) | ||
| if err != nil { | ||
| return 0, slackerror.New(slackerror.ErrInvalidArguments). | ||
| WithMessage("Invalid archive date: %q", dateStr). | ||
| WithRemediation("Use yyyy-mm-dd format (e.g., 2025-12-31)") | ||
| } | ||
| return t.Unix(), nil | ||
| } | ||
|
|
||
| // domainFromName derives domain-safe text from the name of the sandbox (lowercase, alphanumeric + hyphens). | ||
| func domainFromName(name string) string { | ||
| var b []byte | ||
| for _, r := range name { | ||
| if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') { | ||
| b = append(b, byte(r)) | ||
| } else if r >= 'A' && r <= 'Z' { | ||
| b = append(b, byte(r+32)) | ||
|
vegeris marked this conversation as resolved.
Outdated
|
||
| } else if r == ' ' || r == '-' || r == '_' { | ||
| if len(b) > 0 && b[len(b)-1] != '-' { | ||
| b = append(b, '-') | ||
| } | ||
| } | ||
|
vegeris marked this conversation as resolved.
Outdated
|
||
| } | ||
| // Trim leading/trailing hyphens | ||
| for len(b) > 0 && b[0] == '-' { | ||
| b = b[1:] | ||
| } | ||
| for len(b) > 0 && b[len(b)-1] == '-' { | ||
| b = b[:len(b)-1] | ||
| } | ||
|
vegeris marked this conversation as resolved.
Outdated
|
||
| if len(b) == 0 { | ||
| return "sandbox" | ||
| } | ||
| return string(b) | ||
| } | ||
|
|
||
| func printCreateSuccess(cmd *cobra.Command, clients *shared.ClientFactory, teamID, url string) { | ||
| ctx := cmd.Context() | ||
| clients.IO.PrintInfo(ctx, false, "\n%s", style.Sectionf(style.TextSection{ | ||
| Emoji: "beach_with_umbrella", | ||
| Text: " Sandbox Created", | ||
|
vegeris marked this conversation as resolved.
Outdated
|
||
| Secondary: []string{ | ||
| fmt.Sprintf("Team ID: %s", teamID), | ||
| fmt.Sprintf("URL: %s", url), | ||
| }, | ||
| })) | ||
| clients.IO.PrintInfo(ctx, false, "Manage this sandbox from the CLI or visit\n%s", style.Secondary("https://api.slack.com/developer-program/sandboxes")) | ||
| } | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Preview:
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. praise: Excellent formatting choice! |
||
Uh oh!
There was an error while loading. Please reload this page.