Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
138 changes: 132 additions & 6 deletions cmd/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ var (

var instanceSpawnCmd = &cobra.Command{
Use: "spawn <category>/<slug>",
Short: "Spawn a per-team instance of an instanced challenge",
Short: "Spawn a per-team instance of an instanced challenge (remote instancer when instancer.url is set, else local store)",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
category, slug, ok := splitCategorySlug(args[0])
Expand All @@ -50,6 +50,10 @@ var instanceSpawnCmd = &cobra.Command{
return fmt.Errorf("--team is required")
}

if url, ok := remoteInstancerURL(); ok {
return spawnRemoteInstance(cmd.Context(), url, category, slug, flagInstSpawnTeam, flagInstSpawnFlag)
}

cfg, ch, err := loadInstanced(category, slug)
if err != nil {
return err
Expand Down Expand Up @@ -237,9 +241,13 @@ var flagInstExtendBy time.Duration

var instanceExtendCmd = &cobra.Command{
Use: "extend <instance-id>",
Short: "Bump an instance's expiry by --by (default 15m)",
Short: "Bump an instance's expiry by --by (remote instancer when instancer.url is set, else local store)",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if url, ok := remoteInstancerURL(); ok {
return extendRemoteInstance(cmd.Context(), url, args[0], flagInstExtendBy)
}

store, err := openStore()
if err != nil {
return err
Expand All @@ -265,12 +273,16 @@ var instanceExtendCmd = &cobra.Command{

var instanceFlagCmd = &cobra.Command{
Use: "flag <instance-id>",
Short: "Print the flag for a live instance",
Short: "Print the flag for a live instance (remote instancer when instancer.url is set, else local store)",
Long: "Reads the per-team flag captured when the instance was spawned\n" +
"(`--flag` override or the manifest's static flag). Falls back to\n" +
"the manifest if the store record predates the flag field.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if url, ok := remoteInstancerURL(); ok {
return flagRemoteInstance(cmd.Context(), url, args[0])
}

store, err := openStore()
if err != nil {
return err
Expand Down Expand Up @@ -356,13 +368,17 @@ func inspectContainersViaDocker(ctx context.Context, ids []string) ([]containerS

var instancePsCmd = &cobra.Command{
Use: "ps <instance-id>",
Short: "Show live docker state for an instance",
Short: "Show live state for an instance (remote instancer when instancer.url is set, else local store)",
Long: "Looks up the instance in the store and queries `docker ps` for\n" +
"each container in it. Reports up-time / health status, image, and\n" +
"the host-port mapping captured at spawn.\n\n" +
"The flag is intentionally not printed — use `mctf instance flag <id>`.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if url, ok := remoteInstancerURL(); ok {
return psRemoteInstance(cmd.Context(), url, args[0])
}

store, err := openStore()
if err != nil {
return err
Expand Down Expand Up @@ -508,11 +524,121 @@ func remoteInstancerURL() (string, bool) {
return "", false
}

func newRemoteClient(baseURL string) *instancer.APIClient {
return instancer.NewAPIClient(baseURL, os.Getenv("MCTF_INSTANCER_ADMIN_KEY"))
}

// spawnRemoteInstance requests the remote instancer's admin API to spawn a new instance.
func spawnRemoteInstance(ctx context.Context, baseURL, category, slug, teamID, flagOverride string) error {
client := newRemoteClient(baseURL)
challengeID := category + "/" + slug
req := instancer.AdminSpawnRequest{
ChallengeID: challengeID,
TeamID: teamID,
UserID: teamID,
Flag: flagOverride,
}
inst, err := client.Spawn(ctx, req)
if err != nil {
return err
}
if flagJSON {
body, err := json.MarshalIndent(inst, "", " ")
if err != nil {
return err
}
fmt.Println(string(body))
return nil
}
fmt.Printf("spawning %s for team %q (remote: %s)\n", challengeID, teamID, baseURL)
fmt.Printf(" id: %s\n", inst.InstanceID)
if inst.PublicURL != "" {
fmt.Printf(" url: %s\n", inst.PublicURL)
}
if len(inst.Ports) > 0 {
fmt.Printf(" ports: %s\n", remotePortsString(inst.Ports))
}
if inst.ExpiresAt != "" {
fmt.Printf("\nspawn complete (expires %s)\n", inst.ExpiresAt)
} else {
fmt.Printf("\nspawn complete\n")
}
return nil
}

// extendRemoteInstance extends an instance's lifetime via the remote instancer's admin API.
func extendRemoteInstance(ctx context.Context, baseURL, id string, by time.Duration) error {
client := newRemoteClient(baseURL)
durationMinutes := int(by.Minutes())
if durationMinutes <= 0 {
durationMinutes = 1
}
expires, err := client.Extend(ctx, id, durationMinutes)
if err != nil {
return err
}
fmt.Printf("extended %s (remote: %s) — now expires %s\n", id, baseURL, expires)
return nil
}

// flagRemoteInstance reads the per-team flag via the remote instancer's admin API.
func flagRemoteInstance(ctx context.Context, baseURL, id string) error {
client := newRemoteClient(baseURL)
flag, err := client.Flag(ctx, id)
if err != nil {
return err
}
fmt.Println(flag)
return nil
}

// psRemoteInstance fetches instance status via the remote instancer's admin API.
func psRemoteInstance(ctx context.Context, baseURL, id string) error {
client := newRemoteClient(baseURL)
instances, err := client.List(ctx)
if err != nil {
return err
}
for _, inst := range instances {
if inst.InstanceID == id {
if flagJSON {
body, err := json.MarshalIndent(inst, "", " ")
if err != nil {
return err
}
fmt.Println(string(body))
return nil
}
fmt.Printf("instance: %s\n", inst.InstanceID)
fmt.Printf("challenge: %s\n", inst.ChallengeID)
team := inst.TeamName
if team == "" {
team = inst.TeamID
}
if team != "" {
fmt.Printf("team: %s\n", team)
}
fmt.Printf("status: %s\n", inst.Status)
if inst.PublicURL != "" {
fmt.Printf("url: %s\n", inst.PublicURL)
}
if len(inst.Ports) > 0 {
fmt.Printf("ports: %s\n", remotePortsString(inst.Ports))
}
if inst.ExpiresAt != "" {
fmt.Printf("expires: %s\n", inst.ExpiresAt)
}
return nil
}
}
return fmt.Errorf("no instance with id %q found on remote instancer", id)
}

// destroyRemoteInstance force-stops one instance via the remote instancer's
// admin API. The server owns teardown; the local --force/--volumes flags do
// not apply to this path.
func destroyRemoteInstance(ctx context.Context, baseURL, id string) error {
client := instancer.NewAPIClient(baseURL, os.Getenv("MCTF_INSTANCER_ADMIN_KEY"))
client := newRemoteClient(baseURL)
msg, err := client.Destroy(ctx, id)
if err != nil {
return err
Expand All @@ -527,7 +653,7 @@ func destroyRemoteInstance(ctx context.Context, baseURL, id string) error {
// listRemoteInstances lists instances from the remote instancer's admin API.
// The admin key is read from MCTF_INSTANCER_ADMIN_KEY (a secret, never config).
func listRemoteInstances(ctx context.Context, baseURL string) error {
client := instancer.NewAPIClient(baseURL, os.Getenv("MCTF_INSTANCER_ADMIN_KEY"))
client := newRemoteClient(baseURL)
instances, err := client.List(ctx)
if err != nil {
return err
Expand Down
Loading
Loading