Skip to content

API Implementation ​

For people who read or change the CLI's source: how pfy talks to the Manager's GraphQL API, how the code is laid out, and which operations exist. Using the CLI does not require any of it.

Architecture ​

The CLI is a Go program. GraphQL queries and mutations are written as Go structs with graphql field tags; the client library turns them into query text and decodes the response into the same structs.

Components ​

cli/
├── pfy/
│   └── main.go                 # Entry point; classifies errors and sets the exit code
└── internal/
    ├── api/                    # Manager client
    │   ├── client.go           #   GraphQL client with Bearer auth + REST pilet upload
    │   ├── config.go           #   ~/.pfy/config.yaml loader (manager_url, token)
    │   ├── operations.go       #   projects, tenants, applications, language packs, machine users
    │   ├── multitenancy.go     #   environments, Tenant2Application enablement, config copy
    │   ├── deployment.go       #   deployments and their specs
    │   ├── service.go          #   application services
    │   ├── datadep.go          #   managed data stores
    │   ├── comms_config.go     #   communications config layers
    │   └── proxy.go            #   proxy sync status / diff / sync
    ├── app/                    # Commands
    │   ├── app.go              #   Build metadata (Version, BuildTime; set via ldflags)
    │   ├── commands.go         #   Root command, global flags, --version wiring
    │   ├── cmd_*.go            #   One file per command group
    │   ├── deployresolve.go    #   --env/--app/--tenant/--service -> Deployment resolution
    │   ├── deploystack.go      #   nomad deploy --stack ordering
    │   ├── output.go           #   -o text|json|yaml and stderr routing
    │   ├── exit.go             #   exit codes (0, 1, 3, 4)
    │   └── helpers.go          #   client construction, pagination, error classification
    ├── auth/                   # Browser (device-flow) login
    ├── project/                # project.yaml handling and semver bumping
    ├── render/                 # Deployment spec -> Nomad job HCL
    ├── adopt/                  # Hand-written Nomad job -> deployment spec
    ├── platform/               # install.yaml -> platform stack (nomad / compose)
    ├── nomad/                  # nomad CLI wrappers (validate, plan, run, inspect, legacy image rewrite)
    ├── dev/                    # pfy dev: compose stack, seeding, dev proxy
    ├── pilet/                  # pfy pilet create scaffolding
    └── templates/              # Embedded CI/CD, Dockerfile, and Nomad job templates

GraphQL Client ​

Implementation ​

Built on hasura/go-graphql-client:

go
type Client struct {
    gqlClient  *graphql.Client
    httpClient *http.Client
    endpoint   string
    token      string
}

The client maintains both a GraphQL client for API operations and an HTTP client for REST endpoints (e.g., pilet uploads).

Authentication ​

Bearer token authentication via WithRequestModifier:

go
gqlClient = gqlClient.WithRequestModifier(func(r *http.Request) {
    r.Header.Set("Authorization", "Bearer "+token)
})

Type-Safe Operations ​

Struct-Based Queries ​

GraphQL operations are defined as Go structs with graphql tags:

go
func (c *Client) GetProjects(ctx context.Context, filters []Filter, order Order, pagination Pagination) ([]Project, error) {
    var query struct {
        Projects []Project `graphql:"projects(filters: $filters, order: $order, pagination: $pagination)"`
    }
    // ... execute query with variables
    return query.Projects, nil
}

Domain Types ​

The CLI defines domain types that map to GraphQL types:

go
type Project struct {
    ID   graphql.ID     `graphql:"id" json:"id"`
    Slug graphql.String `graphql:"slug" json:"slug"`
    Name graphql.String `graphql:"name" json:"name"`
}

type Application struct {
    ID      graphql.ID     `graphql:"id" json:"id"`
    Slug    graphql.String `graphql:"slug" json:"slug"`
    Name    graphql.String `graphql:"name" json:"name"`
    Version graphql.String `graphql:"version" json:"version"`
}
// ... similar for Tenant, LanguagePack

GraphQL Types ​

Type mapping from Go to GraphQL:

Go TypeGraphQL TypeUsage
graphql.StringStringText fields
graphql.IDIDUnique identifiers
graphql.IntIntInteger numbers
graphql.BooleanBooleanTrue/false values
graphql.FloatFloatDecimal numbers

All domain types use graphql tags for field mapping and json tags for JSON serialization.

Mutations ​

Mutations use input types:

go
type CreateProjectInput struct {
    Slug        graphql.String `json:"slug"`
    Name        graphql.String `json:"name"`
    Description graphql.String `json:"description,omitempty"`
}

func (c *Client) CreateProject(ctx context.Context, input CreateProjectInput) (*Project, error) {
    var mutation struct {
        CreateProject Project `graphql:"createProject(input: $input)"`
    }
    // ... execute mutation
    return &mutation.CreateProject, nil
}

Application Copying ​

go
type CopyApplicationInput struct {
    SourceApplicationID graphql.ID     `json:"sourceApplicationId"`
    TargetTenantID      graphql.ID     `json:"targetTenantId"`
    Slug                graphql.String `json:"slug"`
    // ... other fields
}

A PromoteApplicationInput type still exists in operations.go, but nothing sends it: pfy application promote refuses before making a request.

Implemented Operations ​

Projects ​

Queries:

  • GetProjects - List projects with filters, order, pagination
  • GetProject - Get project by slug
  • GetProjectByID - Get project by ID (used by pfy sync)

Mutations:

  • CreateProject - Create new project
  • UpdateProject - Update existing project
  • DeleteProject - Delete project

Tenants ​

Queries:

  • GetTenants - List tenants in a project (by project slug)
  • GetTenant - Get tenant by project ID and slug

Mutations:

  • CreateTenant - Create tenant in project
  • UpdateTenant - Update tenant
  • DeleteTenant - Delete tenant
  • SetTenantDeploymentMode - Switch a tenant between shared and dedicated; returns the DNS checklist

Applications ​

Queries:

  • GetApplications - List applications in a project (by project slug)
  • GetApplication - Get application by project ID and slug

Mutations:

  • CreateApplication - Create application in a project
  • UpdateApplication - Update name, description, and/or version (also what update-version calls)
  • DeleteApplication - Delete application
  • CopyApplication - Copy an application to another tenant

Environments and enablement (multitenancy.go) ​

  • GetEnvironments, CreateEnvironment, UpdateEnvironment, DeleteEnvironment
  • GetTenant2Applications - List an application's tenant enablements
  • SetApplicationEnabledForTenant - Enable/disable an application for a tenant
  • CopyTenant2ApplicationConfig - Copy per-tenant config between enablements

Deployments, services, data stores, comms, proxy ​

  • deployment.go - read deployments and update their spec (UpdateDeployment)
  • service.go - list/create/update/delete an application's services
  • datadep.go - managed data stores and their backup policy
  • comms_config.go - read/write the communications config of a cascade layer
  • proxy.go - proxy sync status, rendered vhost preview, and the sync push

Language Packs ​

Queries:

  • GetLanguagePacks - List language packs in project
  • GetLanguagePack - Get a single language pack

Mutations:

  • CreateLanguagePack - Create language pack
  • DeleteLanguagePack - Delete language pack

Token check ​

  • VerifyToken - Runs a minimal projects query; used by pfy whoami and by pfy login before saving a token

Filters, Ordering, and Pagination ​

Filter, Order, and Pagination ​

go
type Filter struct {
    Field graphql.String `json:"field"`
    Value graphql.String `json:"value"`
}

type Order struct {
    Field     graphql.String `json:"field"`
    Direction graphql.String `json:"direction"` // "ASC" or "DESC"
}

type Pagination struct {
    Offset graphql.Int `json:"offset"`
    Limit  graphql.Int `json:"limit"`
}

Configuration Management ​

Config Structure ​

go
type Config struct {
    ManagerURL string `yaml:"manager_url"`
    Token      string `yaml:"token"`
}

Configuration is loaded from ~/.pfy/config.yaml and can be overridden by environment variables (PFY_MANAGER_URL, PFY_MANAGER_TOKEN). Those two keys are the whole schema.

Configuration Priority ​

  1. Command-line flags (highest priority)
  2. Environment variables
  3. Config file (lowest priority)

Error Handling ​

Each API call wraps its error with the operation name, and attaches the endpoint in an APIError:

go
if err := c.Query(ctx, &query, variables); err != nil {
    return nil, fmt.Errorf("failed to get projects: %w", err)
}

pfy/main.go then runs the error through app.ClassifyError once. HTTP 401/403 becomes a single token rejected by <url> ... sentence, other HTTP statuses become manager at <url> returned HTTP <code>: <body>, connection failures become cannot reach manager at <url> ..., and GraphQL errors are reduced to their messages (locations and paths are dropped). Unrecognised errors pass through unchanged.

Exit codes ​

exit.go defines four codes: 0 success, 1 any failure, and two verdict codes used only by pfy proxy status|diff|sync --exit-code: 3 (state is bad) and 4 (state cannot be determined, or LOCK_BUSY). A verdict is printed without the Error: prefix.

REST Endpoints ​

Pilet Upload ​

The client includes an UploadPilet method for uploading pilet packages via multipart/form-data, with the same Bearer token:

go
func (c *Client) UploadPilet(ctx context.Context, projectID, name, version, description, filePath string) error {
    // Creates multipart form with file upload
    // POST /pilets/{projectID}/{name}/{version}
}

A non-2xx response is returned as upload failed with status <code>: <truncated body>.

Browser login ​

internal/auth implements the device flow used by pfy login: POST /api/cli/login/start with an ephemeral public key, then POST /api/cli/login/poll until the session is approved. The PAT comes back sealed to that key and is decrypted in memory.

Output ​

internal/app/output.go routes every list/get result through printResult. In text mode the command's own formatter runs; in json/yaml mode the typed result is marshalled to stdout and all human-readable chatter (printQuiet, printVerbose) goes to stderr. printData prints the data rows themselves and ignores --quiet.