diff --git a/dagger.gen.go b/dagger.gen.go index aa54ca3b8..664b251bf 100644 --- a/dagger.gen.go +++ b/dagger.gen.go @@ -60,6 +60,9 @@ type Platform string // A unique identifier for a secret. type SecretID string +// A unique service identifier. +type ServiceID string + // A content-addressed socket identifier. type SocketID string @@ -74,27 +77,39 @@ type Void string // Key value object that represents a build argument. type BuildArg struct { // The build argument name. - Name string `json:"name,omitempty"` + Name string `json:"name"` // The build argument value. - Value string `json:"value,omitempty"` + Value string `json:"value"` } type FunctionCallInput struct { // The name of the argument to the function - Name string `json:"name,omitempty"` + Name string `json:"name"` // The value of the argument represented as a string of the JSON serialization. - Value JSON `json:"value,omitempty"` + Value JSON `json:"value"` } // Key value object that represents a Pipeline label. type PipelineLabel struct { // Label name. - Name string `json:"name,omitempty"` + Name string `json:"name"` // Label value. - Value string `json:"value,omitempty"` + Value string `json:"value"` +} + +// Port forwarding rules for tunneling network traffic. +type PortForward struct { + // Destination port for traffic. + Backend int `json:"backend"` + + // Port to expose to clients. If unspecified, a default will be chosen. + Frontend int `json:"frontend"` + + // Protocol to use for traffic. + Protocol NetworkProtocol `json:"protocol,omitempty"` } // A directory whose contents persist across runs. @@ -149,10 +164,8 @@ type Container struct { q *querybuilder.Selection c graphql.Client - endpoint *string envVariable *string export *bool - hostname *string id *ContainerID imageRef *string label *string @@ -174,6 +187,18 @@ func (r *Container) With(f WithContainerFunc) *Container { return f(r) } +// Turn the container into a Service. +// +// Be sure to set any exposed ports before this conversion. +func (r *Container) AsService() *Service { + q := r.q.Select("asService") + + return &Service{ + q: q, + c: r.c, + } +} + // ContainerAsTarballOpts contains options for Container.AsTarball type ContainerAsTarballOpts struct { // Identifiers for other platform specific containers. @@ -288,43 +313,6 @@ func (r *Container) Directory(path string) *Directory { } } -// ContainerEndpointOpts contains options for Container.Endpoint -type ContainerEndpointOpts struct { - // The exposed port number for the endpoint - Port int - // Return a URL with the given scheme, eg. http for http:// - Scheme string -} - -// Retrieves an endpoint that clients can use to reach this container. -// -// If no port is specified, the first exposed port is used. If none exist an error is returned. -// -// If a scheme is specified, a URL is returned. Otherwise, a host:port pair is returned. -// -// Currently experimental; set _EXPERIMENTAL_DAGGER_SERVICES_DNS=0 to disable. -func (r *Container) Endpoint(ctx context.Context, opts ...ContainerEndpointOpts) (string, error) { - if r.endpoint != nil { - return *r.endpoint, nil - } - q := r.q.Select("endpoint") - for i := len(opts) - 1; i >= 0; i-- { - // `port` optional argument - if !querybuilder.IsZeroValue(opts[i].Port) { - q = q.Arg("port", opts[i].Port) - } - // `scheme` optional argument - if !querybuilder.IsZeroValue(opts[i].Scheme) { - q = q.Arg("scheme", opts[i].Scheme) - } - } - - var response string - - q = q.Bind(&response) - return response, q.Execute(ctx, r.c) -} - // Retrieves entrypoint to be prepended to the arguments of all commands. func (r *Container) Entrypoint(ctx context.Context) ([]string, error) { q := r.q.Select("entrypoint") @@ -434,8 +422,6 @@ func (r *Container) Export(ctx context.Context, path string, opts ...ContainerEx // // This includes ports already exposed by the image, even if not // explicitly added with dagger. -// -// Currently experimental; set _EXPERIMENTAL_DAGGER_SERVICES_DNS=0 to disable. func (r *Container) ExposedPorts(ctx context.Context) ([]Port, error) { q := r.q.Select("exposedPorts") @@ -493,21 +479,6 @@ func (r *Container) From(address string) *Container { } } -// Retrieves a hostname which can be used by clients to reach this container. -// -// Currently experimental; set _EXPERIMENTAL_DAGGER_SERVICES_DNS=0 to disable. -func (r *Container) Hostname(ctx context.Context) (string, error) { - if r.hostname != nil { - return *r.hostname, nil - } - q := r.q.Select("hostname") - - var response string - - q = q.Bind(&response) - return response, q.Execute(ctx, r.c) -} - // A unique identifier for this container. func (r *Container) ID(ctx context.Context) (ContainerID, error) { if r.id != nil { @@ -985,8 +956,6 @@ type ContainerWithExposedPortOpts struct { // Exposed ports serve two purposes: // - For health checks and introspection, when running services // - For setting the EXPOSE OCI field when publishing the container -// -// Currently experimental; set _EXPERIMENTAL_DAGGER_SERVICES_DNS=0 to disable. func (r *Container) WithExposedPort(port int, opts ...ContainerWithExposedPortOpts) *Container { q := r.q.Select("withExposedPort") for i := len(opts) - 1; i >= 0; i-- { @@ -1307,9 +1276,7 @@ func (r *Container) WithSecretVariable(name string, secret *Secret) *Container { // The service will be reachable from the container via the provided hostname alias. // // The service dependency will also convey to any files or directories produced by the container. -// -// Currently experimental; set _EXPERIMENTAL_DAGGER_SERVICES_DNS=0 to disable. -func (r *Container) WithServiceBinding(alias string, service *Container) *Container { +func (r *Container) WithServiceBinding(alias string, service *Service) *Container { assertNotNil("service", service) q := r.q.Select("withServiceBinding") q = q.Arg("alias", alias) @@ -1390,8 +1357,6 @@ type ContainerWithoutExposedPortOpts struct { } // Unexpose a previously exposed port. -// -// Currently experimental; set _EXPERIMENTAL_DAGGER_SERVICES_DNS=0 to disable. func (r *Container) WithoutExposedPort(port int, opts ...ContainerWithoutExposedPortOpts) *Container { q := r.q.Select("withoutExposedPort") for i := len(opts) - 1; i >= 0; i-- { @@ -2742,6 +2707,29 @@ func (r *Host) File(path string) *File { } } +// HostServiceOpts contains options for Host.Service +type HostServiceOpts struct { + // Upstream host to forward traffic to. + Host string +} + +// Creates a service that forwards traffic to a specified address via the host. +func (r *Host) Service(ports []PortForward, opts ...HostServiceOpts) *Service { + q := r.q.Select("service") + for i := len(opts) - 1; i >= 0; i-- { + // `host` optional argument + if !querybuilder.IsZeroValue(opts[i].Host) { + q = q.Arg("host", opts[i].Host) + } + } + q = q.Arg("ports", ports) + + return &Service{ + q: q, + c: r.c, + } +} + // Sets a secret given a user-defined name and the file path on the host, and returns the secret. // The file is limited to a size of 512000 bytes. func (r *Host) SetSecretFile(name string, path string) *Secret { @@ -2755,6 +2743,48 @@ func (r *Host) SetSecretFile(name string, path string) *Secret { } } +// HostTunnelOpts contains options for Host.Tunnel +type HostTunnelOpts struct { + // Map each service port to the same port on the host, as if the service were + // running natively. + // + // Note: enabling may result in port conflicts. + Native bool + // Configure explicit port forwarding rules for the tunnel. + // + // If a port's frontend is unspecified or 0, a random port will be chosen by + // the host. + // + // If no ports are given, all of the service's ports are forwarded. If native + // is true, each port maps to the same port on the host. If native is false, + // each port maps to a random port chosen by the host. + // + // If ports are given and native is true, the ports are additive. + Ports []PortForward +} + +// Creates a tunnel that forwards traffic from the host to a service. +func (r *Host) Tunnel(service *Service, opts ...HostTunnelOpts) *Service { + assertNotNil("service", service) + q := r.q.Select("tunnel") + for i := len(opts) - 1; i >= 0; i-- { + // `native` optional argument + if !querybuilder.IsZeroValue(opts[i].Native) { + q = q.Arg("native", opts[i].Native) + } + // `ports` optional argument + if !querybuilder.IsZeroValue(opts[i].Ports) { + q = q.Arg("ports", opts[i].Ports) + } + } + q = q.Arg("service", service) + + return &Service{ + q: q, + c: r.c, + } +} + // Accesses a Unix socket on the host. func (r *Host) UnixSocket(path string) *Socket { q := r.q.Select("unixSocket") @@ -3443,7 +3473,7 @@ type GitOpts struct { // Set to true to keep .git directory. KeepGitDir bool // A service which must be started before the repo is fetched. - ExperimentalServiceHost *Container + ExperimentalServiceHost *Service } // Queries a git repository. @@ -3480,7 +3510,7 @@ func (r *Client) Host() *Host { // HTTPOpts contains options for Client.HTTP type HTTPOpts struct { // A service which must be started before the URL is fetched. - ExperimentalServiceHost *Container + ExperimentalServiceHost *Service } // Returns a file containing an http remote url content. @@ -3599,6 +3629,17 @@ func (r *Client) LoadSecretFromID(id SecretID) *Secret { } } +// Loads a service from ID. +func (r *Client) LoadServiceFromID(id ServiceID) *Service { + q := r.q.Select("loadServiceFromID") + q = q.Arg("id", id) + + return &Service{ + q: q, + c: r.c, + } +} + // Load a Socket from its ID. func (r *Client) LoadSocketFromID(id SocketID) *Socket { q := r.q.Select("loadSocketFromID") @@ -3804,6 +3845,155 @@ func (r *Secret) Plaintext(ctx context.Context) (string, error) { return response, q.Execute(ctx, r.c) } +type Service struct { + q *querybuilder.Selection + c graphql.Client + + endpoint *string + hostname *string + id *ServiceID + start *ServiceID + stop *ServiceID +} + +// ServiceEndpointOpts contains options for Service.Endpoint +type ServiceEndpointOpts struct { + // The exposed port number for the endpoint + Port int + // Return a URL with the given scheme, eg. http for http:// + Scheme string +} + +// Retrieves an endpoint that clients can use to reach this container. +// +// If no port is specified, the first exposed port is used. If none exist an error is returned. +// +// If a scheme is specified, a URL is returned. Otherwise, a host:port pair is returned. +func (r *Service) Endpoint(ctx context.Context, opts ...ServiceEndpointOpts) (string, error) { + if r.endpoint != nil { + return *r.endpoint, nil + } + q := r.q.Select("endpoint") + for i := len(opts) - 1; i >= 0; i-- { + // `port` optional argument + if !querybuilder.IsZeroValue(opts[i].Port) { + q = q.Arg("port", opts[i].Port) + } + // `scheme` optional argument + if !querybuilder.IsZeroValue(opts[i].Scheme) { + q = q.Arg("scheme", opts[i].Scheme) + } + } + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx, r.c) +} + +// Retrieves a hostname which can be used by clients to reach this container. +func (r *Service) Hostname(ctx context.Context) (string, error) { + if r.hostname != nil { + return *r.hostname, nil + } + q := r.q.Select("hostname") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx, r.c) +} + +// A unique identifier for this service. +func (r *Service) ID(ctx context.Context) (ServiceID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.q.Select("id") + + var response ServiceID + + q = q.Bind(&response) + return response, q.Execute(ctx, r.c) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *Service) XXX_GraphQLType() string { + return "Service" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *Service) XXX_GraphQLIDType() string { + return "ServiceID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *Service) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *Service) MarshalJSON() ([]byte, error) { + id, err := r.ID(context.Background()) + if err != nil { + return nil, err + } + return json.Marshal(id) +} + +// Retrieves the list of ports provided by the service. +func (r *Service) Ports(ctx context.Context) ([]Port, error) { + q := r.q.Select("ports") + + q = q.Select("description port protocol") + + type ports struct { + Description string + Port int + Protocol NetworkProtocol + } + + convert := func(fields []ports) []Port { + out := []Port{} + + for i := range fields { + val := Port{description: &fields[i].Description, port: &fields[i].Port, protocol: &fields[i].Protocol} + out = append(out, val) + } + + return out + } + var response []ports + + q = q.Bind(&response) + + err := q.Execute(ctx, r.c) + if err != nil { + return nil, err + } + + return convert(response), nil +} + +// Start the service and wait for its health checks to succeed. +// +// Services bound to a Container do not need to be manually started. +func (r *Service) Start(ctx context.Context) (*Service, error) { + q := r.q.Select("start") + + return r, q.Execute(ctx, r.c) +} + +// Stop the service. +func (r *Service) Stop(ctx context.Context) (*Service, error) { + q := r.q.Select("stop") + + return r, q.Execute(ctx, r.c) +} + type Socket struct { q *querybuilder.Selection c graphql.Client diff --git a/go.mod b/go.mod index b73343f10..5705aeff3 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/adrg/xdg v0.4.0 github.com/stretchr/testify v1.8.3 github.com/vektah/gqlparser/v2 v2.5.6 + golang.org/x/exp v0.0.0-20231006140011-7918f672742d golang.org/x/sync v0.3.0 ) @@ -24,6 +25,6 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/mitchellh/go-homedir v1.1.0 github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/sys v0.12.0 // indirect + golang.org/x/sys v0.13.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 298a9b9b7..16187101c 100644 --- a/go.sum +++ b/go.sum @@ -38,11 +38,13 @@ github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gt github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/vektah/gqlparser/v2 v2.5.6 h1:Ou14T0N1s191eRMZ1gARVqohcbe1e8FrcONScsq8cRU= github.com/vektah/gqlparser/v2 v2.5.6/go.mod h1:z8xXUff237NntSuH8mLFijZ+1tjV1swDbpDqjJmk6ME= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/internal/querybuilder/marshal.go b/internal/querybuilder/marshal.go index 3eabf9775..df013e5f8 100644 --- a/internal/querybuilder/marshal.go +++ b/internal/querybuilder/marshal.go @@ -9,6 +9,7 @@ import ( "strings" gqlgen "github.com/99designs/gqlgen/graphql" + "golang.org/x/exp/slices" "golang.org/x/sync/errgroup" ) @@ -97,12 +98,17 @@ func marshalValue(ctx context.Context, v reflect.Value) (string, error) { i := i eg.Go(func() error { f := t.Field(i) + fv := v.Field(i) name := f.Name - tag := strings.SplitN(f.Tag.Get("json"), ",", 2)[0] - if tag != "" { - name = tag + jsonTag := strings.Split(f.Tag.Get("json"), ",") + if jsonTag[0] != "" { + name = jsonTag[0] } - m, err := marshalValue(gctx, v.Field(i)) + isOptional := slices.Contains(jsonTag[1:], "omitempty") + if isOptional && IsZeroValue(fv.Interface()) { + return nil + } + m, err := marshalValue(gctx, fv) if err != nil { return err }