Skip to content

Commit fe95799

Browse files
authored
Add cross-org replay commands (#4708)
* Add cross-org replay commands * bump fly-go * fixup innmem * tidy
1 parent 8e47e0d commit fe95799

File tree

11 files changed

+454
-5
lines changed

11 files changed

+454
-5
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ require (
7474
github.com/spf13/pflag v1.0.9
7575
github.com/spf13/viper v1.20.1
7676
github.com/stretchr/testify v1.11.1
77-
github.com/superfly/fly-go v0.1.70
77+
github.com/superfly/fly-go v0.1.71
7878
github.com/superfly/graphql v0.2.6
7979
github.com/superfly/lfsc-go v0.1.1
8080
github.com/superfly/macaroon v0.3.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -637,8 +637,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
637637
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
638638
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
639639
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
640-
github.com/superfly/fly-go v0.1.70 h1:JHeVxPXE/Cf2ori9mLXGe1NKyA81Ov4arRl7JLQB2RE=
641-
github.com/superfly/fly-go v0.1.70/go.mod h1:2gCFoNR3iUELADGTJtbBoviMa2jlh2vlPK3cKUajOp8=
640+
github.com/superfly/fly-go v0.1.71 h1:F0rn8W7vq3/FFGGGCp4FVDTKGX9pxD0deVLHeMiRMRs=
641+
github.com/superfly/fly-go v0.1.71/go.mod h1:2gCFoNR3iUELADGTJtbBoviMa2jlh2vlPK3cKUajOp8=
642642
github.com/superfly/graphql v0.2.6 h1:zppbodNerWecoXEdjkhrqaNaSjGqobhXNlViHFuZzb4=
643643
github.com/superfly/graphql v0.2.6/go.mod h1:CVfDl31srm8HnJ9udwLu6hFNUW/P6GUM2dKcG1YQ8jc=
644644
github.com/superfly/lfsc-go v0.1.1 h1:dGjLgt81D09cG+aR9lJZIdmonjZSR5zYCi7s54+ZU2Q=

gql/schema.graphql

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,37 @@ enum AccessTokenType {
8484
ui
8585
}
8686

87+
"""
88+
Autogenerated input type of AddAllowedReplaySourceOrgs
89+
"""
90+
input AddAllowedReplaySourceOrgsInput {
91+
"""
92+
Slugs of orgs to add to the allowed replay sources list
93+
"""
94+
allowedOrgSlugs: [String!]!
95+
96+
"""
97+
A unique identifier for the client performing the mutation.
98+
"""
99+
clientMutationId: String
100+
101+
"""
102+
Slug of the org to configure (the target org)
103+
"""
104+
organizationSlug: String!
105+
}
106+
107+
"""
108+
Autogenerated return type of AddAllowedReplaySourceOrgs.
109+
"""
110+
type AddAllowedReplaySourceOrgsPayload {
111+
"""
112+
A unique identifier for the client performing the mutation.
113+
"""
114+
clientMutationId: String
115+
organization: Organization!
116+
}
117+
87118
"""
88119
Autogenerated return type of AddCertificate.
89120
"""
@@ -1256,6 +1287,11 @@ type AppCertificate implements Node {
12561287
"""
12571288
last: Int
12581289
): CertificateConnection!
1290+
1291+
"""
1292+
If rate limited by the certificate provider, when the certificate can be retried
1293+
"""
1294+
rateLimitedUntil: ISO8601DateTime
12591295
source: String
12601296
validationErrors: [AppCertificateValidationError!]!
12611297
}
@@ -2099,6 +2135,11 @@ enum CertificateValidationErrorCodeEnum {
20992135
"""
21002136
NO_ALLOCATED_IPS
21012137

2138+
"""
2139+
Let's Encrypt rate limit exceeded. Too many certificate requests for this hostname
2140+
"""
2141+
RATE_LIMITED
2142+
21022143
"""
21032144
Service exposing port 443 does not have a TLS handler configured
21042145
"""
@@ -4350,14 +4391,13 @@ type DummyWireGuardPeerPayload {
43504391
}
43514392

43524393
type EgressIPAddress implements Node {
4353-
createdAt: ISO8601DateTime!
4354-
43554394
"""
43564395
ID of the object.
43574396
"""
43584397
id: ID!
43594398
ip: String!
43604399
region: String!
4400+
updatedAt: ISO8601DateTime!
43614401
version: Int!
43624402
}
43634403

@@ -5961,6 +6001,12 @@ type MoveAppPayload {
59616001
}
59626002

59636003
type Mutations {
6004+
addAllowedReplaySourceOrgs(
6005+
"""
6006+
Parameters for AddAllowedReplaySourceOrgs
6007+
"""
6008+
input: AddAllowedReplaySourceOrgsInput!
6009+
): AddAllowedReplaySourceOrgsPayload
59646010
addCertificate(
59656011
"""
59666012
The application to attach the new hostname to
@@ -6561,6 +6607,12 @@ type Mutations {
65616607
"""
65626608
input: ReleaseManagedServiceIPAddressInput!
65636609
): ReleaseManagedServiceIPAddressPayload
6610+
removeAllowedReplaySourceOrgs(
6611+
"""
6612+
Parameters for RemoveAllowedReplaySourceOrgs
6613+
"""
6614+
input: RemoveAllowedReplaySourceOrgsInput!
6615+
): RemoveAllowedReplaySourceOrgsPayload
65646616
removeMachine(
65656617
"""
65666618
Parameters for RemoveMachine
@@ -6933,6 +6985,11 @@ type Organization implements Node {
69336985
Check if the organization has agreed to the extension provider terms of service
69346986
"""
69356987
agreedToProviderTos(providerName: String!): Boolean!
6988+
6989+
"""
6990+
Slugs of organizations allowed to send replays to this org
6991+
"""
6992+
allowedReplaySourceOrgSlugs: [String!]!
69366993
apps(
69376994
"""
69386995
Returns the elements in the list that come after the specified cursor.
@@ -8599,6 +8656,37 @@ type RemoteDockerBuilderAppRole implements AppRole {
85998656
name: String!
86008657
}
86018658

8659+
"""
8660+
Autogenerated input type of RemoveAllowedReplaySourceOrgs
8661+
"""
8662+
input RemoveAllowedReplaySourceOrgsInput {
8663+
"""
8664+
A unique identifier for the client performing the mutation.
8665+
"""
8666+
clientMutationId: String
8667+
8668+
"""
8669+
Slugs of orgs to remove from the allowed replay sources list
8670+
"""
8671+
orgSlugsToRemove: [String!]!
8672+
8673+
"""
8674+
Slug of the org to configure (the target org)
8675+
"""
8676+
organizationSlug: String!
8677+
}
8678+
8679+
"""
8680+
Autogenerated return type of RemoveAllowedReplaySourceOrgs.
8681+
"""
8682+
type RemoveAllowedReplaySourceOrgsPayload {
8683+
"""
8684+
A unique identifier for the client performing the mutation.
8685+
"""
8686+
clientMutationId: String
8687+
organization: Organization!
8688+
}
8689+
86028690
"""
86038691
Autogenerated input type of RemoveMachine
86048692
"""

internal/command/orgs/orgs.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Organization admins can also invite or remove users from Organizations.
3838
newRemove(),
3939
newCreate(),
4040
newDelete(),
41+
newReplaySources(),
4142
)
4243

4344
return orgs
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package orgs
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
"github.com/superfly/flyctl/internal/command"
6+
)
7+
8+
func newReplaySources() *cobra.Command {
9+
const (
10+
long = `Commands for managing cross-organization replay permissions.`
11+
short = "Manage allowed replay source organizations"
12+
)
13+
14+
cmd := command.New("replay-sources", short, long, nil)
15+
16+
cmd.AddCommand(
17+
newReplaySourcesList(),
18+
newReplaySourcesAdd(),
19+
newReplaySourcesRemove(),
20+
)
21+
22+
return cmd
23+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package orgs
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/spf13/cobra"
9+
10+
fly "github.com/superfly/fly-go"
11+
"github.com/superfly/flyctl/internal/command"
12+
"github.com/superfly/flyctl/internal/flag"
13+
"github.com/superfly/flyctl/internal/flyutil"
14+
"github.com/superfly/flyctl/internal/prompt"
15+
"github.com/superfly/flyctl/iostreams"
16+
)
17+
18+
func newReplaySourcesAdd() *cobra.Command {
19+
const (
20+
long = `Add organizations to the list of allowed replay sources for this organization.
21+
22+
If no slugs are provided, an interactive selector will be shown.`
23+
short = "Add allowed replay source organizations"
24+
usage = "add [<slug>...]"
25+
)
26+
27+
cmd := command.New(usage, short, long, runReplaySourcesAdd,
28+
command.RequireSession,
29+
)
30+
31+
cmd.Args = cobra.ArbitraryArgs
32+
33+
flag.Add(cmd,
34+
flag.Org(),
35+
)
36+
37+
return cmd
38+
}
39+
40+
func runReplaySourcesAdd(ctx context.Context) error {
41+
client := flyutil.ClientFromContext(ctx)
42+
io := iostreams.FromContext(ctx)
43+
44+
org, err := OrgFromFlagOrSelect(ctx, fly.AdminOnly)
45+
if err != nil {
46+
return err
47+
}
48+
49+
args := flag.Args(ctx)
50+
var sourceOrgSlugs []string
51+
52+
if len(args) == 0 {
53+
// Interactive mode: show multi-select of available orgs
54+
userOrgs, err := client.GetOrganizations(ctx)
55+
if err != nil {
56+
return fmt.Errorf("failed to get organizations: %w", err)
57+
}
58+
59+
// Get currently allowed orgs to exclude them
60+
currentAllowed, err := client.GetAllowedReplaySourceOrgSlugs(ctx, org.RawSlug)
61+
if err != nil {
62+
return fmt.Errorf("failed to get current replay sources: %w", err)
63+
}
64+
currentSet := make(map[string]bool)
65+
for _, slug := range currentAllowed {
66+
currentSet[slug] = true
67+
}
68+
69+
var options []string
70+
var availableSlugs []string
71+
for _, userOrg := range userOrgs {
72+
if userOrg.RawSlug != org.RawSlug && !currentSet[userOrg.RawSlug] {
73+
options = append(options, fmt.Sprintf("%s (%s)", userOrg.Name, userOrg.RawSlug))
74+
availableSlugs = append(availableSlugs, userOrg.RawSlug)
75+
}
76+
}
77+
78+
if len(options) == 0 {
79+
fmt.Fprintln(io.Out, "No organizations available to add")
80+
return nil
81+
}
82+
83+
var selections []int
84+
if err := prompt.MultiSelect(ctx, &selections, "Select organizations to add:", nil, options...); err != nil {
85+
return err
86+
}
87+
88+
if len(selections) == 0 {
89+
return nil
90+
}
91+
92+
for _, idx := range selections {
93+
sourceOrgSlugs = append(sourceOrgSlugs, availableSlugs[idx])
94+
}
95+
} else {
96+
// Use positional arguments
97+
sourceOrgSlugs = args
98+
}
99+
100+
_, err = client.AddAllowedReplaySourceOrgs(ctx, org.RawSlug, sourceOrgSlugs)
101+
if err != nil {
102+
return fmt.Errorf("failed to add allowed replay source orgs: %w", err)
103+
}
104+
105+
fmt.Fprintf(io.Out, "Added allowed replay source organizations for %s: %s\n",
106+
org.RawSlug, strings.Join(sourceOrgSlugs, ", "))
107+
108+
return nil
109+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package orgs
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/spf13/cobra"
8+
9+
fly "github.com/superfly/fly-go"
10+
"github.com/superfly/flyctl/internal/command"
11+
"github.com/superfly/flyctl/internal/config"
12+
"github.com/superfly/flyctl/internal/flag"
13+
"github.com/superfly/flyctl/internal/flyutil"
14+
"github.com/superfly/flyctl/internal/render"
15+
"github.com/superfly/flyctl/iostreams"
16+
)
17+
18+
func newReplaySourcesList() *cobra.Command {
19+
const (
20+
long = `List organizations allowed to replay requests to this organization.`
21+
short = "List allowed replay source organizations"
22+
usage = "list"
23+
)
24+
25+
cmd := command.New(usage, short, long, runReplaySourcesList,
26+
command.RequireSession,
27+
)
28+
29+
cmd.Aliases = []string{"ls"}
30+
flag.Add(cmd, flag.Org(), flag.JSONOutput())
31+
32+
return cmd
33+
}
34+
35+
func runReplaySourcesList(ctx context.Context) error {
36+
client := flyutil.ClientFromContext(ctx)
37+
38+
org, err := OrgFromFlagOrSelect(ctx, fly.AdminOnly)
39+
if err != nil {
40+
return err
41+
}
42+
43+
sourceOrgSlugs, err := client.GetAllowedReplaySourceOrgSlugs(ctx, org.RawSlug)
44+
if err != nil {
45+
return fmt.Errorf("failed to get allowed replay source orgs: %w", err)
46+
}
47+
48+
io := iostreams.FromContext(ctx)
49+
cfg := config.FromContext(ctx)
50+
51+
if cfg.JSONOutput {
52+
return render.JSON(io.Out, sourceOrgSlugs)
53+
}
54+
55+
if len(sourceOrgSlugs) == 0 {
56+
fmt.Fprintf(io.Out, "No replay source organizations configured for %s\n", org.RawSlug)
57+
return nil
58+
}
59+
60+
fmt.Fprintf(io.Out, "Allowed replay source organizations for %s:\n", org.RawSlug)
61+
for _, slug := range sourceOrgSlugs {
62+
fmt.Fprintf(io.Out, " %s\n", slug)
63+
}
64+
65+
return nil
66+
}

0 commit comments

Comments
 (0)