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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ require (
github.com/spf13/pflag v1.0.9
github.com/spf13/viper v1.20.1
github.com/stretchr/testify v1.11.1
github.com/superfly/fly-go v0.1.70
github.com/superfly/fly-go v0.1.71
github.com/superfly/graphql v0.2.6
github.com/superfly/lfsc-go v0.1.1
github.com/superfly/macaroon v0.3.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -637,8 +637,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/superfly/fly-go v0.1.70 h1:JHeVxPXE/Cf2ori9mLXGe1NKyA81Ov4arRl7JLQB2RE=
github.com/superfly/fly-go v0.1.70/go.mod h1:2gCFoNR3iUELADGTJtbBoviMa2jlh2vlPK3cKUajOp8=
github.com/superfly/fly-go v0.1.71 h1:F0rn8W7vq3/FFGGGCp4FVDTKGX9pxD0deVLHeMiRMRs=
github.com/superfly/fly-go v0.1.71/go.mod h1:2gCFoNR3iUELADGTJtbBoviMa2jlh2vlPK3cKUajOp8=
github.com/superfly/graphql v0.2.6 h1:zppbodNerWecoXEdjkhrqaNaSjGqobhXNlViHFuZzb4=
github.com/superfly/graphql v0.2.6/go.mod h1:CVfDl31srm8HnJ9udwLu6hFNUW/P6GUM2dKcG1YQ8jc=
github.com/superfly/lfsc-go v0.1.1 h1:dGjLgt81D09cG+aR9lJZIdmonjZSR5zYCi7s54+ZU2Q=
Expand Down
92 changes: 90 additions & 2 deletions gql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,37 @@ enum AccessTokenType {
ui
}

"""
Autogenerated input type of AddAllowedReplaySourceOrgs
"""
input AddAllowedReplaySourceOrgsInput {
"""
Slugs of orgs to add to the allowed replay sources list
"""
allowedOrgSlugs: [String!]!

"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String

"""
Slug of the org to configure (the target org)
"""
organizationSlug: String!
}

"""
Autogenerated return type of AddAllowedReplaySourceOrgs.
"""
type AddAllowedReplaySourceOrgsPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
organization: Organization!
}

"""
Autogenerated return type of AddCertificate.
"""
Expand Down Expand Up @@ -1256,6 +1287,11 @@ type AppCertificate implements Node {
"""
last: Int
): CertificateConnection!

"""
If rate limited by the certificate provider, when the certificate can be retried
"""
rateLimitedUntil: ISO8601DateTime
source: String
validationErrors: [AppCertificateValidationError!]!
}
Expand Down Expand Up @@ -2099,6 +2135,11 @@ enum CertificateValidationErrorCodeEnum {
"""
NO_ALLOCATED_IPS

"""
Let's Encrypt rate limit exceeded. Too many certificate requests for this hostname
"""
RATE_LIMITED

"""
Service exposing port 443 does not have a TLS handler configured
"""
Expand Down Expand Up @@ -4350,14 +4391,13 @@ type DummyWireGuardPeerPayload {
}

type EgressIPAddress implements Node {
createdAt: ISO8601DateTime!

"""
ID of the object.
"""
id: ID!
ip: String!
region: String!
updatedAt: ISO8601DateTime!
version: Int!
}

Expand Down Expand Up @@ -5961,6 +6001,12 @@ type MoveAppPayload {
}

type Mutations {
addAllowedReplaySourceOrgs(
"""
Parameters for AddAllowedReplaySourceOrgs
"""
input: AddAllowedReplaySourceOrgsInput!
): AddAllowedReplaySourceOrgsPayload
addCertificate(
"""
The application to attach the new hostname to
Expand Down Expand Up @@ -6561,6 +6607,12 @@ type Mutations {
"""
input: ReleaseManagedServiceIPAddressInput!
): ReleaseManagedServiceIPAddressPayload
removeAllowedReplaySourceOrgs(
"""
Parameters for RemoveAllowedReplaySourceOrgs
"""
input: RemoveAllowedReplaySourceOrgsInput!
): RemoveAllowedReplaySourceOrgsPayload
removeMachine(
"""
Parameters for RemoveMachine
Expand Down Expand Up @@ -6933,6 +6985,11 @@ type Organization implements Node {
Check if the organization has agreed to the extension provider terms of service
"""
agreedToProviderTos(providerName: String!): Boolean!

"""
Slugs of organizations allowed to send replays to this org
"""
allowedReplaySourceOrgSlugs: [String!]!
apps(
"""
Returns the elements in the list that come after the specified cursor.
Expand Down Expand Up @@ -8599,6 +8656,37 @@ type RemoteDockerBuilderAppRole implements AppRole {
name: String!
}

"""
Autogenerated input type of RemoveAllowedReplaySourceOrgs
"""
input RemoveAllowedReplaySourceOrgsInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String

"""
Slugs of orgs to remove from the allowed replay sources list
"""
orgSlugsToRemove: [String!]!

"""
Slug of the org to configure (the target org)
"""
organizationSlug: String!
}

"""
Autogenerated return type of RemoveAllowedReplaySourceOrgs.
"""
type RemoveAllowedReplaySourceOrgsPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
organization: Organization!
}

"""
Autogenerated input type of RemoveMachine
"""
Expand Down
1 change: 1 addition & 0 deletions internal/command/orgs/orgs.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Organization admins can also invite or remove users from Organizations.
newRemove(),
newCreate(),
newDelete(),
newReplaySources(),
)

return orgs
Expand Down
23 changes: 23 additions & 0 deletions internal/command/orgs/replay_sources.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package orgs

import (
"github.com/spf13/cobra"
"github.com/superfly/flyctl/internal/command"
)

func newReplaySources() *cobra.Command {
const (
long = `Commands for managing cross-organization replay permissions.`
short = "Manage allowed replay source organizations"
)

cmd := command.New("replay-sources", short, long, nil)

cmd.AddCommand(
newReplaySourcesList(),
newReplaySourcesAdd(),
newReplaySourcesRemove(),
)

return cmd
}
109 changes: 109 additions & 0 deletions internal/command/orgs/replay_sources_add.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package orgs

import (
"context"
"fmt"
"strings"

"github.com/spf13/cobra"

fly "github.com/superfly/fly-go"
"github.com/superfly/flyctl/internal/command"
"github.com/superfly/flyctl/internal/flag"
"github.com/superfly/flyctl/internal/flyutil"
"github.com/superfly/flyctl/internal/prompt"
"github.com/superfly/flyctl/iostreams"
)

func newReplaySourcesAdd() *cobra.Command {
const (
long = `Add organizations to the list of allowed replay sources for this organization.

If no slugs are provided, an interactive selector will be shown.`
short = "Add allowed replay source organizations"
usage = "add [<slug>...]"
)

cmd := command.New(usage, short, long, runReplaySourcesAdd,
command.RequireSession,
)

cmd.Args = cobra.ArbitraryArgs

flag.Add(cmd,
flag.Org(),
)

return cmd
}

func runReplaySourcesAdd(ctx context.Context) error {
client := flyutil.ClientFromContext(ctx)
io := iostreams.FromContext(ctx)

org, err := OrgFromFlagOrSelect(ctx, fly.AdminOnly)
if err != nil {
return err
}

args := flag.Args(ctx)
var sourceOrgSlugs []string

if len(args) == 0 {
// Interactive mode: show multi-select of available orgs
userOrgs, err := client.GetOrganizations(ctx)
if err != nil {
return fmt.Errorf("failed to get organizations: %w", err)
}

// Get currently allowed orgs to exclude them
currentAllowed, err := client.GetAllowedReplaySourceOrgSlugs(ctx, org.RawSlug)
if err != nil {
return fmt.Errorf("failed to get current replay sources: %w", err)
}
currentSet := make(map[string]bool)
for _, slug := range currentAllowed {
currentSet[slug] = true
}

var options []string
var availableSlugs []string
for _, userOrg := range userOrgs {
if userOrg.RawSlug != org.RawSlug && !currentSet[userOrg.RawSlug] {
options = append(options, fmt.Sprintf("%s (%s)", userOrg.Name, userOrg.RawSlug))
availableSlugs = append(availableSlugs, userOrg.RawSlug)
}
}

if len(options) == 0 {
fmt.Fprintln(io.Out, "No organizations available to add")
return nil
}

var selections []int
if err := prompt.MultiSelect(ctx, &selections, "Select organizations to add:", nil, options...); err != nil {
return err
}

if len(selections) == 0 {
return nil
}

for _, idx := range selections {
sourceOrgSlugs = append(sourceOrgSlugs, availableSlugs[idx])
}
} else {
// Use positional arguments
sourceOrgSlugs = args
}

_, err = client.AddAllowedReplaySourceOrgs(ctx, org.RawSlug, sourceOrgSlugs)
if err != nil {
return fmt.Errorf("failed to add allowed replay source orgs: %w", err)
}

fmt.Fprintf(io.Out, "Added allowed replay source organizations for %s: %s\n",
org.RawSlug, strings.Join(sourceOrgSlugs, ", "))

return nil
}
66 changes: 66 additions & 0 deletions internal/command/orgs/replay_sources_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package orgs

import (
"context"
"fmt"

"github.com/spf13/cobra"

fly "github.com/superfly/fly-go"
"github.com/superfly/flyctl/internal/command"
"github.com/superfly/flyctl/internal/config"
"github.com/superfly/flyctl/internal/flag"
"github.com/superfly/flyctl/internal/flyutil"
"github.com/superfly/flyctl/internal/render"
"github.com/superfly/flyctl/iostreams"
)

func newReplaySourcesList() *cobra.Command {
const (
long = `List organizations allowed to replay requests to this organization.`
short = "List allowed replay source organizations"
usage = "list"
)

cmd := command.New(usage, short, long, runReplaySourcesList,
command.RequireSession,
)

cmd.Aliases = []string{"ls"}
flag.Add(cmd, flag.Org(), flag.JSONOutput())

return cmd
}

func runReplaySourcesList(ctx context.Context) error {
client := flyutil.ClientFromContext(ctx)

org, err := OrgFromFlagOrSelect(ctx, fly.AdminOnly)
if err != nil {
return err
}

sourceOrgSlugs, err := client.GetAllowedReplaySourceOrgSlugs(ctx, org.RawSlug)
if err != nil {
return fmt.Errorf("failed to get allowed replay source orgs: %w", err)
}

io := iostreams.FromContext(ctx)
cfg := config.FromContext(ctx)

if cfg.JSONOutput {
return render.JSON(io.Out, sourceOrgSlugs)
}

if len(sourceOrgSlugs) == 0 {
fmt.Fprintf(io.Out, "No replay source organizations configured for %s\n", org.RawSlug)
return nil
}

fmt.Fprintf(io.Out, "Allowed replay source organizations for %s:\n", org.RawSlug)
for _, slug := range sourceOrgSlugs {
fmt.Fprintf(io.Out, " %s\n", slug)
}

return nil
}
Loading
Loading