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
5 changes: 5 additions & 0 deletions .changeset/better-lines-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@gram/cli": minor
---

Support function uploads
28 changes: 24 additions & 4 deletions cli/internal/api/assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func NewAssetsClient(options *AssetsClientOptions) *AssetsClient {
return &AssetsClient{client: client}
}

type UploadOpenAPIv3Request struct {
type UploadAssetForm struct {
APIKey string
ProjectSlug string
Reader io.ReadCloser
Expand All @@ -50,8 +50,8 @@ type UploadOpenAPIv3Request struct {

func (c *AssetsClient) UploadOpenAPIv3(
ctx context.Context,
req *UploadOpenAPIv3Request,
) (*assets.UploadOpenAPIv3Result, error) {
req *UploadAssetForm,
) (*assets.Asset, error) {
payload := &assets.UploadOpenAPIv3Form{
ApikeyToken: &req.APIKey,
ProjectSlugInput: &req.ProjectSlug,
Expand All @@ -65,5 +65,25 @@ func (c *AssetsClient) UploadOpenAPIv3(
return nil, fmt.Errorf("failed to upload OpenAPI asset: %w", err)
}

return result, nil
return result.Asset, nil
}

func (c *AssetsClient) UploadFunctions(
ctx context.Context,
req *UploadAssetForm,
) (*assets.Asset, error) {
payload := &assets.UploadFunctionsForm{
ApikeyToken: &req.APIKey,
ProjectSlugInput: &req.ProjectSlug,
SessionToken: nil,
ContentType: req.ContentType,
ContentLength: req.ContentLength,
}

result, err := c.client.UploadFunctions(ctx, payload, req.Reader)
if err != nil {
return nil, fmt.Errorf("failed to upload OpenAPI asset: %w", err)
}

return result.Asset, nil
}
16 changes: 9 additions & 7 deletions cli/internal/api/deployments.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ type CreateDeploymentRequest struct {
ProjectSlug string
IdempotencyKey string
OpenAPIv3Assets []*deployments.AddOpenAPIv3DeploymentAssetForm
Functions []*deployments.AddFunctionsForm
}

// CreateDeployment creates a remote deployment.
Expand All @@ -65,7 +66,7 @@ func (c *DeploymentsClient) CreateDeployment(
ProjectSlugInput: &req.ProjectSlug,
IdempotencyKey: req.IdempotencyKey,
Openapiv3Assets: req.OpenAPIv3Assets,
Functions: []*deployments.AddFunctionsForm{},
Functions: req.Functions,
SessionToken: nil,
GithubRepo: nil,
GithubPr: nil,
Expand Down Expand Up @@ -168,10 +169,11 @@ func (c *DeploymentsClient) GetActiveDeployment(

// EvolveRequest lists the assets to add to a deployment.
type EvolveRequest struct {
Assets []*deployments.AddOpenAPIv3DeploymentAssetForm
APIKey secret.Secret
DeploymentID string
ProjectSlug string
OpenAPIv3Assets []*deployments.AddOpenAPIv3DeploymentAssetForm
Functions []*deployments.AddFunctionsForm
APIKey secret.Secret
DeploymentID string
ProjectSlug string
}

// Evolve adds assets to an existing deployment.
Expand All @@ -184,8 +186,8 @@ func (c *DeploymentsClient) Evolve(
ApikeyToken: &key,
ProjectSlugInput: &req.ProjectSlug,
DeploymentID: &req.DeploymentID,
UpsertOpenapiv3Assets: req.Assets,
UpsertFunctions: []*deployments.AddFunctionsForm{},
UpsertOpenapiv3Assets: req.OpenAPIv3Assets,
UpsertFunctions: req.Functions,
ExcludeOpenapiv3Assets: []string{},
ExcludeFunctions: []string{},
ExcludePackages: []string{},
Expand Down
6 changes: 6 additions & 0 deletions cli/internal/app/upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ Example:
Usage: "The URL-friendly slug for the asset",
Required: true,
},
&cli.StringFlag{
Name: "runtime",
Usage: "Runtime to use for function execution (required for functions)",
Required: false,
},
},
Action: func(c *cli.Context) error {
ctx, cancel := signal.NotifyContext(
Expand Down Expand Up @@ -102,5 +107,6 @@ func parseSource(c *cli.Context) deploy.Source {
Location: c.String("location"),
Name: c.String("name"),
Slug: c.String("slug"),
Runtime: c.String("runtime"),
}
}
18 changes: 16 additions & 2 deletions cli/internal/deploy/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ type SourceType string

const (
SourceTypeOpenAPIV3 SourceType = "openapiv3"
SourceTypeFunction SourceType = "function"
)

var AllowedTypes = []SourceType{SourceTypeOpenAPIV3}
var AllowedTypes = []SourceType{SourceTypeOpenAPIV3, SourceTypeFunction}

type Source struct {
Type SourceType `json:"type" yaml:"type" toml:"type"`
Expand All @@ -38,12 +39,20 @@ type Source struct {

// Slug is the human readable public id of the asset.
Slug string `json:"slug" yaml:"slug" toml:"slug"`

// Runtime is the runtime to use for function execution (required for functions).
// Allowed values are: nodejs:22, python:3.12
Runtime string `json:"runtime,omitempty" yaml:"runtime,omitempty" toml:"runtime,omitempty"`
}

// Validate returns an error if the source is missing required fields.
func (s Source) Validate() error {
if !isSupportedType(s) {
return fmt.Errorf("source has unsupported type %q (allowed types: %s)", s.Type, SourceTypeOpenAPIV3)
return fmt.Errorf(
"source has unsupported type %q (allowed types: %v)",
s.Type,
AllowedTypes,
)
}

if s.Location == "" {
Expand All @@ -55,6 +64,11 @@ func (s Source) Validate() error {
if s.Slug == "" {
return fmt.Errorf("source is missing required field 'slug'")
}
if s.Type == SourceTypeFunction && s.Runtime == "" {
return fmt.Errorf(
"source of type 'function' is missing required field 'runtime' (allowed values: nodejs:22, python:3.12)",
)
}
return nil
}

Expand Down
126 changes: 105 additions & 21 deletions cli/internal/deploy/example_usage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ func TestSource_MissingRequiredFields(t *testing.T) {
Location: "/path/to/file",
Name: "",
Slug: "valid-slug",
Runtime: "nodejs:22",
},
wantErr: "source is missing required field 'name'",
},
Expand All @@ -31,6 +32,7 @@ func TestSource_MissingRequiredFields(t *testing.T) {
Location: "/path/to/file",
Name: "Valid Name",
Slug: "",
Runtime: "nodejs:22",
},
wantErr: "source is missing required field 'slug'",
},
Expand All @@ -41,16 +43,40 @@ func TestSource_MissingRequiredFields(t *testing.T) {
Location: "/path/to/file",
Name: "",
Slug: "",
Runtime: "nodejs:22",
},
wantErr: "source is missing required field 'name'",
},
{
name: "valid source",
name: "missing runtime for function",
source: Source{
Type: SourceTypeFunction,
Location: "/path/to/file.zip",
Name: "Example function",
Slug: "example-function",
Runtime: "",
},
wantErr: "source of type 'function' is missing required field 'runtime'",
},
{
name: "valid function",
source: Source{
Type: SourceTypeFunction,
Location: "/path/to/file.zip",
Name: "Example function",
Slug: "example-function",
Runtime: "nodejs:22",
},
wantErr: "",
},
{
name: "valid openapiv3",
source: Source{
Type: SourceTypeOpenAPIV3,
Location: "/path/to/file",
Name: "Valid Name",
Slug: "valid-slug",
Runtime: "nodejs:22",
},
wantErr: "",
},
Expand Down Expand Up @@ -83,28 +109,86 @@ func TestValidateUniqueNames(t *testing.T) {
{
name: "unique names",
sources: []Source{
{Type: SourceTypeOpenAPIV3, Location: "/path/one", Name: "API One", Slug: "api-one"},
{Type: SourceTypeOpenAPIV3, Location: "/path/two", Name: "API Two", Slug: "api-two"},
{Type: SourceTypeOpenAPIV3, Location: "/path/three", Name: "API Three", Slug: "api-three"},
{Type: SourceTypeOpenAPIV3, Location: "/path/one", Name: "API One", Slug: "api-one", Runtime: "nodejs:22"},
{Type: SourceTypeOpenAPIV3, Location: "/path/two", Name: "API Two", Slug: "api-two", Runtime: "nodejs:22"},
{Type: SourceTypeOpenAPIV3, Location: "/path/three", Name: "API Three", Slug: "api-three", Runtime: "nodejs:22"},
},
wantErr: "",
},
{
name: "duplicate names",
sources: []Source{
{Type: SourceTypeOpenAPIV3, Location: "/path/one", Name: "Duplicate API", Slug: "api-one", Runtime: "nodejs:22"},
{Type: SourceTypeOpenAPIV3, Location: "/path/two", Name: "API Two", Slug: "api-two", Runtime: "nodejs:22"},
{Type: SourceTypeOpenAPIV3, Location: "/path/three", Name: "Duplicate API", Slug: "api-three", Runtime: "nodejs:22"},
},
wantErr: "source names must be unique: ['Duplicate API' (2 times)]",
},
{
name: "multiple duplicate names",
sources: []Source{
{Type: SourceTypeOpenAPIV3, Location: "/path/one", Name: "First Duplicate", Slug: "api-one", Runtime: "nodejs:22"},
{Type: SourceTypeOpenAPIV3, Location: "/path/two", Name: "Second Duplicate", Slug: "api-two", Runtime: "nodejs:22"},
{Type: SourceTypeOpenAPIV3, Location: "/path/three", Name: "First Duplicate", Slug: "api-three", Runtime: "nodejs:22"},
{Type: SourceTypeOpenAPIV3, Location: "/path/four", Name: "Second Duplicate", Slug: "api-four", Runtime: "nodejs:22"},
},
wantErr: "source names must be unique:",
},
{
name: "empty sources",
sources: []Source{},
wantErr: "",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

err := validateUniqueNames(tt.sources)

if tt.wantErr == "" {
require.NoError(t, err)
} else {
require.Error(t, err)
require.Contains(t, err.Error(), tt.wantErr)
}
})
}
}
func TestFunctionMissingRuntime(t *testing.T) {
t.Parallel()

tests := []struct {
name string
sources []Source
wantErr string
}{
{
name: "",
sources: []Source{
{Type: SourceTypeOpenAPIV3, Location: "/path/one", Name: "API One", Slug: "api-one", Runtime: "nodejs:22"},
{Type: SourceTypeOpenAPIV3, Location: "/path/two", Name: "API Two", Slug: "api-two", Runtime: "nodejs:22"},
{Type: SourceTypeOpenAPIV3, Location: "/path/three", Name: "API Three", Slug: "api-three", Runtime: "nodejs:22"},
},
wantErr: "",
},
{
name: "duplicate names",
sources: []Source{
{Type: SourceTypeOpenAPIV3, Location: "/path/one", Name: "Duplicate API", Slug: "api-one"},
{Type: SourceTypeOpenAPIV3, Location: "/path/two", Name: "API Two", Slug: "api-two"},
{Type: SourceTypeOpenAPIV3, Location: "/path/three", Name: "Duplicate API", Slug: "api-three"},
{Type: SourceTypeOpenAPIV3, Location: "/path/one", Name: "Duplicate API", Slug: "api-one", Runtime: "nodejs:22"},
{Type: SourceTypeOpenAPIV3, Location: "/path/two", Name: "API Two", Slug: "api-two", Runtime: "nodejs:22"},
{Type: SourceTypeOpenAPIV3, Location: "/path/three", Name: "Duplicate API", Slug: "api-three", Runtime: "nodejs:22"},
},
wantErr: "source names must be unique: ['Duplicate API' (2 times)]",
},
{
name: "multiple duplicate names",
sources: []Source{
{Type: SourceTypeOpenAPIV3, Location: "/path/one", Name: "First Duplicate", Slug: "api-one"},
{Type: SourceTypeOpenAPIV3, Location: "/path/two", Name: "Second Duplicate", Slug: "api-two"},
{Type: SourceTypeOpenAPIV3, Location: "/path/three", Name: "First Duplicate", Slug: "api-three"},
{Type: SourceTypeOpenAPIV3, Location: "/path/four", Name: "Second Duplicate", Slug: "api-four"},
{Type: SourceTypeOpenAPIV3, Location: "/path/one", Name: "First Duplicate", Slug: "api-one", Runtime: "nodejs:22"},
{Type: SourceTypeOpenAPIV3, Location: "/path/two", Name: "Second Duplicate", Slug: "api-two", Runtime: "nodejs:22"},
{Type: SourceTypeOpenAPIV3, Location: "/path/three", Name: "First Duplicate", Slug: "api-three", Runtime: "nodejs:22"},
{Type: SourceTypeOpenAPIV3, Location: "/path/four", Name: "Second Duplicate", Slug: "api-four", Runtime: "nodejs:22"},
},
wantErr: "source names must be unique:",
},
Expand Down Expand Up @@ -142,28 +226,28 @@ func TestValidateUniqueSlugs(t *testing.T) {
{
name: "unique slugs",
sources: []Source{
{Type: SourceTypeOpenAPIV3, Location: "/path/one", Name: "API One", Slug: "api-one"},
{Type: SourceTypeOpenAPIV3, Location: "/path/two", Name: "API Two", Slug: "api-two"},
{Type: SourceTypeOpenAPIV3, Location: "/path/three", Name: "API Three", Slug: "api-three"},
{Type: SourceTypeOpenAPIV3, Location: "/path/one", Name: "API One", Slug: "api-one", Runtime: "nodejs:22"},
{Type: SourceTypeOpenAPIV3, Location: "/path/two", Name: "API Two", Slug: "api-two", Runtime: "nodejs:22"},
{Type: SourceTypeOpenAPIV3, Location: "/path/three", Name: "API Three", Slug: "api-three", Runtime: "nodejs:22"},
},
wantErr: "",
},
{
name: "duplicate slugs",
sources: []Source{
{Type: SourceTypeOpenAPIV3, Location: "/path/one", Name: "API One", Slug: "duplicate-slug"},
{Type: SourceTypeOpenAPIV3, Location: "/path/two", Name: "API Two", Slug: "api-two"},
{Type: SourceTypeOpenAPIV3, Location: "/path/three", Name: "API Three", Slug: "duplicate-slug"},
{Type: SourceTypeOpenAPIV3, Location: "/path/one", Name: "API One", Slug: "duplicate-slug", Runtime: "nodejs:22"},
{Type: SourceTypeOpenAPIV3, Location: "/path/two", Name: "API Two", Slug: "api-two", Runtime: "nodejs:22"},
{Type: SourceTypeOpenAPIV3, Location: "/path/three", Name: "API Three", Slug: "duplicate-slug", Runtime: "nodejs:22"},
},
wantErr: "source slugs must be unique: ['duplicate-slug' (2 times)]",
},
{
name: "multiple duplicate slugs",
sources: []Source{
{Type: SourceTypeOpenAPIV3, Location: "/path/one", Name: "API One", Slug: "first-duplicate"},
{Type: SourceTypeOpenAPIV3, Location: "/path/two", Name: "API Two", Slug: "second-duplicate"},
{Type: SourceTypeOpenAPIV3, Location: "/path/three", Name: "API Three", Slug: "first-duplicate"},
{Type: SourceTypeOpenAPIV3, Location: "/path/four", Name: "API Four", Slug: "second-duplicate"},
{Type: SourceTypeOpenAPIV3, Location: "/path/one", Name: "API One", Slug: "first-duplicate", Runtime: "nodejs:22"},
{Type: SourceTypeOpenAPIV3, Location: "/path/two", Name: "API Two", Slug: "second-duplicate", Runtime: "nodejs:22"},
{Type: SourceTypeOpenAPIV3, Location: "/path/three", Name: "API Three", Slug: "first-duplicate", Runtime: "nodejs:22"},
{Type: SourceTypeOpenAPIV3, Location: "/path/four", Name: "API Four", Slug: "second-duplicate", Runtime: "nodejs:22"},
},
wantErr: "source slugs must be unique:",
},
Expand Down
5 changes: 5 additions & 0 deletions cli/internal/deploy/source_reader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ info:
Location: testFile,
Name: "Test API",
Slug: "test-api",
Runtime: "nodejs:22",
}

reader := NewSourceReader(source)
Expand Down Expand Up @@ -73,6 +74,7 @@ func TestSourceReader_JSONFile(t *testing.T) {
Location: testFile,
Name: "Test JSON API",
Slug: "test-json-api",
Runtime: "nodejs:22",
}

reader := NewSourceReader(source)
Expand Down Expand Up @@ -101,6 +103,7 @@ func TestSourceReader_NonexistentFile(t *testing.T) {
Location: "/nonexistent/path/file.yaml",
Name: "Nonexistent API",
Slug: "nonexistent-api",
Runtime: "nodejs:22",
}

reader := NewSourceReader(source)
Expand Down Expand Up @@ -132,6 +135,7 @@ func TestSourceReader_RemoteURL(t *testing.T) {
Location: staticServer.URL + "/api-spec.yaml",
Name: "Remote API",
Slug: "remote-api",
Runtime: "nodejs:22",
}

reader := NewSourceReader(source)
Expand Down Expand Up @@ -165,6 +169,7 @@ func TestSourceReader_RemoteURL_Error(t *testing.T) {
Location: server.URL + "/nonexistent.yaml",
Name: "Remote API",
Slug: "remote-api",
Runtime: "nodejs:22",
}

reader := NewSourceReader(source)
Expand Down
Loading