diff --git a/go/apps/ctrl/run.go b/go/apps/ctrl/run.go index 0d97485509..0e2f561ff9 100644 --- a/go/apps/ctrl/run.go +++ b/go/apps/ctrl/run.go @@ -10,6 +10,7 @@ import ( "connectrpc.com/connect" "github.com/unkeyed/unkey/go/apps/ctrl/services/ctrl" + "github.com/unkeyed/unkey/go/apps/ctrl/services/openapi" "github.com/unkeyed/unkey/go/apps/ctrl/services/version" deployTLS "github.com/unkeyed/unkey/go/deploy/pkg/tls" "github.com/unkeyed/unkey/go/gen/proto/ctrl/v1/ctrlv1connect" @@ -154,19 +155,13 @@ func Run(ctx context.Context, cfg Config) error { return fmt.Errorf("unable to register deployment workflow: %w", err) } - // Create the service implementations - ctrlSvc := ctrl.New(cfg.InstanceID, database) - versionSvc := version.New(database, hydraEngine, logger) - // Create the connect handler mux := http.NewServeMux() // Create the service handlers with interceptors - ctrlPath, ctrlHandler := ctrlv1connect.NewCtrlServiceHandler(ctrlSvc) - mux.Handle(ctrlPath, ctrlHandler) - - versionPath, versionHandler := ctrlv1connect.NewVersionServiceHandler(versionSvc) - mux.Handle(versionPath, versionHandler) + mux.Handle(ctrlv1connect.NewCtrlServiceHandler(ctrl.New(cfg.InstanceID, database))) + mux.Handle(ctrlv1connect.NewVersionServiceHandler(version.New(database, hydraEngine, logger))) + mux.Handle(ctrlv1connect.NewOpenApiServiceHandler(openapi.New(database, logger))) // Configure server addr := fmt.Sprintf(":%d", cfg.HttpPort) diff --git a/go/apps/ctrl/services/openapi/convert.go b/go/apps/ctrl/services/openapi/convert.go new file mode 100644 index 0000000000..d9e968da63 --- /dev/null +++ b/go/apps/ctrl/services/openapi/convert.go @@ -0,0 +1,57 @@ +package openapi + +import ( + "github.com/oasdiff/oasdiff/checker" + "github.com/oasdiff/oasdiff/diff" + ctrlv1 "github.com/unkeyed/unkey/go/gen/proto/ctrl/v1" + "github.com/unkeyed/unkey/go/pkg/ptr" +) + +func convertSummaryToProto(summary *diff.Summary) *ctrlv1.DiffSummary { + // Helper function to get counts safely + getCounts := func(name string) *ctrlv1.DiffCounts { + if details, exists := summary.Details[diff.DetailName(name)]; exists { + return &ctrlv1.DiffCounts{ + Added: int32(details.Added), + Deleted: int32(details.Deleted), + Modified: int32(details.Modified), + } + } + return &ctrlv1.DiffCounts{Added: 0, Deleted: 0, Modified: 0} + } + + return &ctrlv1.DiffSummary{ + Diff: summary.Diff, + Details: &ctrlv1.DiffDetails{ + Endpoints: getCounts("endpoints"), + Paths: getCounts("paths"), + Schemas: getCounts("schemas"), + }, + } +} + +func convertChangesToProto(changes checker.Changes) []*ctrlv1.ChangelogEntry { + localizer := checker.NewLocalizer("en") + result := make([]*ctrlv1.ChangelogEntry, len(changes)) + + for i, change := range changes { + level := int32(1) // INFO + switch change.GetLevel() { + case checker.WARN: + level = 2 + case checker.ERR: + level = 3 + } + + result[i] = &ctrlv1.ChangelogEntry{ + Id: change.GetId(), + Text: change.GetUncolorizedText(localizer), + Level: level, + Operation: change.GetOperation(), + Path: change.GetPath(), + OperationId: ptr.P(change.GetOperationId()), + } + } + + return result +} diff --git a/go/apps/ctrl/services/openapi/get_diff.go b/go/apps/ctrl/services/openapi/get_diff.go new file mode 100644 index 0000000000..6b6040e3bb --- /dev/null +++ b/go/apps/ctrl/services/openapi/get_diff.go @@ -0,0 +1,84 @@ +package openapi + +import ( + "context" + + "connectrpc.com/connect" + "github.com/getkin/kin-openapi/openapi3" + "github.com/oasdiff/oasdiff/checker" + "github.com/oasdiff/oasdiff/diff" + ctrlv1 "github.com/unkeyed/unkey/go/gen/proto/ctrl/v1" + "github.com/unkeyed/unkey/go/pkg/fault" +) + +func (s *Service) GetOpenApiDiff(ctx context.Context, req *connect.Request[ctrlv1.GetOpenApiDiffRequest]) (*connect.Response[ctrlv1.GetOpenApiDiffResponse], error) { + // Load old version spec + oldSpec, err := s.loadVersionSpec(ctx, req.Msg.OldVersionId) + if err != nil { + return nil, connect.NewError(connect.CodeNotFound, fault.Wrap(err, + fault.Internal("failed to load old version spec"), + fault.Public("Old version not found"), + )) + } + + // Load new version spec + newSpec, err := s.loadVersionSpec(ctx, req.Msg.NewVersionId) + if err != nil { + return nil, connect.NewError(connect.CodeNotFound, fault.Wrap(err, + fault.Internal("failed to load new version spec"), + fault.Public("New version not found"), + )) + } + + // Parse OpenAPI specs + loader := openapi3.NewLoader() + loader.IsExternalRefsAllowed = true + + s1, err := loader.LoadFromData([]byte(oldSpec)) + if err != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, fault.Wrap(err, + fault.Internal("failed to parse old OpenAPI spec"), + fault.Public("Invalid OpenAPI specification in old version"), + )) + } + + s2, err := loader.LoadFromData([]byte(newSpec)) + if err != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, fault.Wrap(err, + fault.Internal("failed to parse new OpenAPI spec"), + fault.Public("Invalid OpenAPI specification in new version"), + )) + } + + // Generate diff report + diffReport, err := diff.Get(&diff.Config{}, s1, s2) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, fault.Wrap(err, + fault.Internal("failed to generate diff report"), + fault.Public("Failed to generate diff report"), + )) + } + + // Generate changelog using checker + config := checker.NewConfig(checker.GetAllChecks()) + changes := checker.CheckBackwardCompatibility( + config, + diffReport, + &diff.OperationsSourcesMap{}, + ) + + // Check if there are any breaking changes + hasBreakingChanges := false + for _, change := range changes { + if change.GetLevel() == checker.ERR { + hasBreakingChanges = true + break + } + } + + return connect.NewResponse(&ctrlv1.GetOpenApiDiffResponse{ + Summary: convertSummaryToProto(diffReport.GetSummary()), + HasBreakingChanges: hasBreakingChanges, + Changes: convertChangesToProto(changes), + }), nil +} diff --git a/go/apps/ctrl/services/openapi/service.go b/go/apps/ctrl/services/openapi/service.go new file mode 100644 index 0000000000..b2970ba10c --- /dev/null +++ b/go/apps/ctrl/services/openapi/service.go @@ -0,0 +1,21 @@ +package openapi + +import ( + "github.com/unkeyed/unkey/go/gen/proto/ctrl/v1/ctrlv1connect" + "github.com/unkeyed/unkey/go/pkg/db" + "github.com/unkeyed/unkey/go/pkg/otel/logging" +) + +type Service struct { + ctrlv1connect.UnimplementedOpenApiServiceHandler + db db.Database + logger logging.Logger +} + +func New(database db.Database, logger logging.Logger) *Service { + return &Service{ + UnimplementedOpenApiServiceHandler: ctrlv1connect.UnimplementedOpenApiServiceHandler{}, + db: database, + logger: logger, + } +} diff --git a/go/apps/ctrl/services/openapi/utils.go b/go/apps/ctrl/services/openapi/utils.go new file mode 100644 index 0000000000..a415f99a55 --- /dev/null +++ b/go/apps/ctrl/services/openapi/utils.go @@ -0,0 +1,23 @@ +package openapi + +import ( + "context" + + "github.com/unkeyed/unkey/go/pkg/db" + "github.com/unkeyed/unkey/go/pkg/fault" +) + +func (s *Service) loadVersionSpec(ctx context.Context, versionID string) (string, error) { + version, err := db.Query.FindVersionById(ctx, s.db.RO(), versionID) + if err != nil { + return "", err + } + + if !version.OpenapiSpec.Valid { + return "", fault.New("version has no OpenAPI spec stored", + fault.Public("OpenAPI specification not available for this version"), + ) + } + + return version.OpenapiSpec.String, nil +} diff --git a/go/apps/ctrl/services/version/deploy_workflow.go b/go/apps/ctrl/services/version/deploy_workflow.go index 577515aa3c..950f61862c 100644 --- a/go/apps/ctrl/services/version/deploy_workflow.go +++ b/go/apps/ctrl/services/version/deploy_workflow.go @@ -4,6 +4,8 @@ import ( "context" "database/sql" "fmt" + "io" + "net/http" "strings" "time" @@ -524,6 +526,152 @@ func (w *DeployWorkflow) Run(ctx hydra.WorkflowContext, req *DeployRequest) erro return err } + // Step 19.1: Health check container (using host port mapping) + err = hydra.StepVoid(ctx, "health-check-container", func(stepCtx context.Context) error { + if vmInfo.NetworkInfo == nil || len(vmInfo.NetworkInfo.PortMappings) == 0 { + return fmt.Errorf("no port mappings available for container health check") + } + + // Find the port mapping for container port 8080 + var hostPort int32 + for _, portMapping := range vmInfo.NetworkInfo.PortMappings { + if portMapping.ContainerPort == 8080 { + hostPort = portMapping.HostPort + break + } + } + + if hostPort == 0 { + return fmt.Errorf("no host port mapping found for container port 8080") + } + + // Try multiple host addresses to reach the Docker host + // Prioritize Docker's magic domain names + hostAddresses := []string{ + "host.docker.internal", // Docker Desktop (Windows/Mac) and some Linux setups + "gateway.docker.internal", // Docker gateway + "172.17.0.1", // Default Docker bridge gateway + "172.18.0.1", // Alternative Docker bridge + } + + client := &http.Client{Timeout: 10 * time.Second} + + for _, hostAddr := range hostAddresses { + healthURL := fmt.Sprintf("http://%s:%d/v1/liveness", hostAddr, hostPort) + w.logger.Info("trying container health check", "url", healthURL, "host_port", hostPort, "version_id", req.VersionID) + + resp, err := client.Get(healthURL) + if err != nil { + w.logger.Warn("health check failed for host address", "error", err, "host_addr", hostAddr, "version_id", req.VersionID) + continue + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusOK { + w.logger.Info("container is healthy", "host_addr", hostAddr, "version_id", req.VersionID) + return nil + } + + w.logger.Warn("health check returned non-200 status", "status", resp.StatusCode, "host_addr", hostAddr, "version_id", req.VersionID) + } + + return fmt.Errorf("health check failed on all host addresses: %v", hostAddresses) + }) + if err != nil { + w.logger.Error("container health check failed", "error", err, "version_id", req.VersionID) + // Don't fail the deployment, just skip OpenAPI scraping + } + + // Step 19.2: Scrape OpenAPI spec from container (using host port mapping) + openapiSpec, err := hydra.Step(ctx, "scrape-openapi-spec", func(stepCtx context.Context) (string, error) { + if vmInfo.NetworkInfo == nil || len(vmInfo.NetworkInfo.PortMappings) == 0 { + w.logger.Warn("no port mappings available for OpenAPI scraping", "version_id", req.VersionID) + return "", nil + } + + // Find the port mapping for container port 8080 + var hostPort int32 + for _, portMapping := range vmInfo.NetworkInfo.PortMappings { + if portMapping.ContainerPort == 8080 { + hostPort = portMapping.HostPort + break + } + } + + if hostPort == 0 { + w.logger.Warn("no host port mapping found for container port 8080", "version_id", req.VersionID) + return "", nil + } + + // Try multiple host addresses to reach the Docker host + hostAddresses := []string{ + "host.docker.internal", // Docker Desktop (Windows/Mac) and some Linux setups + "gateway.docker.internal", // Docker gateway + "172.17.0.1", // Default Docker bridge gateway + "172.18.0.1", // Alternative Docker bridge + } + + client := &http.Client{Timeout: 10 * time.Second} + + for _, hostAddr := range hostAddresses { + openapiURL := fmt.Sprintf("http://%s:%d/openapi.yaml", hostAddr, hostPort) + w.logger.Info("trying to scrape OpenAPI spec", "url", openapiURL, "host_port", hostPort, "version_id", req.VersionID) + + resp, err := client.Get(openapiURL) + if err != nil { + w.logger.Warn("OpenAPI scraping failed for host address", "error", err, "host_addr", hostAddr, "version_id", req.VersionID) + continue + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + w.logger.Warn("OpenAPI endpoint returned non-200 status", "status", resp.StatusCode, "host_addr", hostAddr, "version_id", req.VersionID) + continue + } + + // Read the OpenAPI spec + specBytes, err := io.ReadAll(resp.Body) + if err != nil { + w.logger.Warn("failed to read OpenAPI spec response", "error", err, "host_addr", hostAddr, "version_id", req.VersionID) + continue + } + + w.logger.Info("OpenAPI spec scraped successfully", "host_addr", hostAddr, "version_id", req.VersionID, "spec_size", len(specBytes)) + return string(specBytes), nil + } + + return "", fmt.Errorf("failed to scrape OpenAPI spec from all host addresses: %v", hostAddresses) + }) + if err != nil { + w.logger.Error("failed to scrape OpenAPI spec", "error", err, "version_id", req.VersionID) + return err + } + + // Step 19.3: Store OpenAPI spec in database + err = hydra.StepVoid(ctx, "store-openapi-spec", func(stepCtx context.Context) error { + if openapiSpec == "" { + w.logger.Info("no OpenAPI spec to store", "version_id", req.VersionID) + return nil + } + + // Store in database + err := db.Query.UpdateVersionOpenApiSpec(stepCtx, w.db.RW(), db.UpdateVersionOpenApiSpecParams{ + ID: req.VersionID, + OpenapiSpec: sql.NullString{String: openapiSpec, Valid: true}, + }) + if err != nil { + w.logger.Warn("failed to store OpenAPI spec in database", "error", err, "version_id", req.VersionID) + return nil // Don't fail the deployment + } + + w.logger.Info("OpenAPI spec stored in database successfully", "version_id", req.VersionID, "spec_size", len(openapiSpec)) + return nil + }) + if err != nil { + w.logger.Error("failed to store OpenAPI spec", "error", err, "version_id", req.VersionID) + return err + } + // Step 20: Log completed err = hydra.StepVoid(ctx, "log-completed", func(stepCtx context.Context) error { return db.Query.InsertVersionStep(stepCtx, w.db.RW(), db.InsertVersionStepParams{ diff --git a/go/demo_api/main.go b/go/demo_api/main.go index 5f95978dae..c0ea1fd098 100644 --- a/go/demo_api/main.go +++ b/go/demo_api/main.go @@ -41,6 +41,77 @@ func main() { json.NewEncoder(w).Encode(response) }) + // OpenAPI spec endpoint + mux.HandleFunc("/openapi.yaml", func(w http.ResponseWriter, r *http.Request) { + spec := `openapi: 3.0.3 +info: + title: Demo API + description: A simple demo API for testing deployments + version: 1.0.0 + contact: + name: Unkey Support + email: support@unkey.com +servers: + - url: /v1 + description: API v1 +paths: + /liveness: + get: + operationId: getLiveness + summary: Health check endpoint + description: Returns OK if the service is healthy + responses: + '200': + description: Service is healthy + content: + text/plain: + schema: + type: string + example: OK + /hello: + get: + operationId: getHello + summary: Hello endpoint + description: Returns a greeting message with timestamp + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + message: + type: number + example: Hello from demo API + timestamp: + type: string + format: date-time + example: 2023-12-07T10:30:00Z + required: + - message + - timestamp +components: + schemas: + HelloResponse: + type: object + properties: + message: + type: string + description: The greeting message + timestamp: + type: string + format: date-time + description: The current timestamp + required: + - message + - timestamp` + + w.Header().Set("Content-Type", "application/yaml") + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, spec) + }) + log.Printf("Demo API starting on port %s", port) server := &http.Server{ @@ -51,4 +122,4 @@ func main() { if err := server.ListenAndServe(); err != nil { log.Fatal("Failed to start server:", err) } -} \ No newline at end of file +} diff --git a/go/gen/proto/ctrl/v1/ctrlv1connect/openapi.connect.go b/go/gen/proto/ctrl/v1/ctrlv1connect/openapi.connect.go new file mode 100644 index 0000000000..7f473db132 --- /dev/null +++ b/go/gen/proto/ctrl/v1/ctrlv1connect/openapi.connect.go @@ -0,0 +1,113 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: proto/ctrl/v1/openapi.proto + +package ctrlv1connect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + v1 "github.com/unkeyed/unkey/go/gen/proto/ctrl/v1" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // OpenApiServiceName is the fully-qualified name of the OpenApiService service. + OpenApiServiceName = "ctrl.v1.OpenApiService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // OpenApiServiceGetOpenApiDiffProcedure is the fully-qualified name of the OpenApiService's + // GetOpenApiDiff RPC. + OpenApiServiceGetOpenApiDiffProcedure = "/ctrl.v1.OpenApiService/GetOpenApiDiff" +) + +// These variables are the protoreflect.Descriptor objects for the RPCs defined in this package. +var ( + openApiServiceServiceDescriptor = v1.File_proto_ctrl_v1_openapi_proto.Services().ByName("OpenApiService") + openApiServiceGetOpenApiDiffMethodDescriptor = openApiServiceServiceDescriptor.Methods().ByName("GetOpenApiDiff") +) + +// OpenApiServiceClient is a client for the ctrl.v1.OpenApiService service. +type OpenApiServiceClient interface { + GetOpenApiDiff(context.Context, *connect.Request[v1.GetOpenApiDiffRequest]) (*connect.Response[v1.GetOpenApiDiffResponse], error) +} + +// NewOpenApiServiceClient constructs a client for the ctrl.v1.OpenApiService service. By default, +// it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and +// sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() +// or connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewOpenApiServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) OpenApiServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + return &openApiServiceClient{ + getOpenApiDiff: connect.NewClient[v1.GetOpenApiDiffRequest, v1.GetOpenApiDiffResponse]( + httpClient, + baseURL+OpenApiServiceGetOpenApiDiffProcedure, + connect.WithSchema(openApiServiceGetOpenApiDiffMethodDescriptor), + connect.WithClientOptions(opts...), + ), + } +} + +// openApiServiceClient implements OpenApiServiceClient. +type openApiServiceClient struct { + getOpenApiDiff *connect.Client[v1.GetOpenApiDiffRequest, v1.GetOpenApiDiffResponse] +} + +// GetOpenApiDiff calls ctrl.v1.OpenApiService.GetOpenApiDiff. +func (c *openApiServiceClient) GetOpenApiDiff(ctx context.Context, req *connect.Request[v1.GetOpenApiDiffRequest]) (*connect.Response[v1.GetOpenApiDiffResponse], error) { + return c.getOpenApiDiff.CallUnary(ctx, req) +} + +// OpenApiServiceHandler is an implementation of the ctrl.v1.OpenApiService service. +type OpenApiServiceHandler interface { + GetOpenApiDiff(context.Context, *connect.Request[v1.GetOpenApiDiffRequest]) (*connect.Response[v1.GetOpenApiDiffResponse], error) +} + +// NewOpenApiServiceHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewOpenApiServiceHandler(svc OpenApiServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + openApiServiceGetOpenApiDiffHandler := connect.NewUnaryHandler( + OpenApiServiceGetOpenApiDiffProcedure, + svc.GetOpenApiDiff, + connect.WithSchema(openApiServiceGetOpenApiDiffMethodDescriptor), + connect.WithHandlerOptions(opts...), + ) + return "/ctrl.v1.OpenApiService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case OpenApiServiceGetOpenApiDiffProcedure: + openApiServiceGetOpenApiDiffHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedOpenApiServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedOpenApiServiceHandler struct{} + +func (UnimplementedOpenApiServiceHandler) GetOpenApiDiff(context.Context, *connect.Request[v1.GetOpenApiDiffRequest]) (*connect.Response[v1.GetOpenApiDiffResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ctrl.v1.OpenApiService.GetOpenApiDiff is not implemented")) +} diff --git a/go/gen/proto/ctrl/v1/openapi.pb.go b/go/gen/proto/ctrl/v1/openapi.pb.go new file mode 100644 index 0000000000..22532022a9 --- /dev/null +++ b/go/gen/proto/ctrl/v1/openapi.pb.go @@ -0,0 +1,487 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.6 +// protoc (unknown) +// source: proto/ctrl/v1/openapi.proto + +package ctrlv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type GetOpenApiDiffRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + OldVersionId string `protobuf:"bytes,1,opt,name=old_version_id,json=oldVersionId,proto3" json:"old_version_id,omitempty"` + NewVersionId string `protobuf:"bytes,2,opt,name=new_version_id,json=newVersionId,proto3" json:"new_version_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetOpenApiDiffRequest) Reset() { + *x = GetOpenApiDiffRequest{} + mi := &file_proto_ctrl_v1_openapi_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetOpenApiDiffRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetOpenApiDiffRequest) ProtoMessage() {} + +func (x *GetOpenApiDiffRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_ctrl_v1_openapi_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetOpenApiDiffRequest.ProtoReflect.Descriptor instead. +func (*GetOpenApiDiffRequest) Descriptor() ([]byte, []int) { + return file_proto_ctrl_v1_openapi_proto_rawDescGZIP(), []int{0} +} + +func (x *GetOpenApiDiffRequest) GetOldVersionId() string { + if x != nil { + return x.OldVersionId + } + return "" +} + +func (x *GetOpenApiDiffRequest) GetNewVersionId() string { + if x != nil { + return x.NewVersionId + } + return "" +} + +type ChangelogEntry struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Text string `protobuf:"bytes,2,opt,name=text,proto3" json:"text,omitempty"` + Level int32 `protobuf:"varint,3,opt,name=level,proto3" json:"level,omitempty"` + Operation string `protobuf:"bytes,4,opt,name=operation,proto3" json:"operation,omitempty"` + OperationId *string `protobuf:"bytes,5,opt,name=operation_id,json=operationId,proto3,oneof" json:"operation_id,omitempty"` + Path string `protobuf:"bytes,6,opt,name=path,proto3" json:"path,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChangelogEntry) Reset() { + *x = ChangelogEntry{} + mi := &file_proto_ctrl_v1_openapi_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChangelogEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChangelogEntry) ProtoMessage() {} + +func (x *ChangelogEntry) ProtoReflect() protoreflect.Message { + mi := &file_proto_ctrl_v1_openapi_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChangelogEntry.ProtoReflect.Descriptor instead. +func (*ChangelogEntry) Descriptor() ([]byte, []int) { + return file_proto_ctrl_v1_openapi_proto_rawDescGZIP(), []int{1} +} + +func (x *ChangelogEntry) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *ChangelogEntry) GetText() string { + if x != nil { + return x.Text + } + return "" +} + +func (x *ChangelogEntry) GetLevel() int32 { + if x != nil { + return x.Level + } + return 0 +} + +func (x *ChangelogEntry) GetOperation() string { + if x != nil { + return x.Operation + } + return "" +} + +func (x *ChangelogEntry) GetOperationId() string { + if x != nil && x.OperationId != nil { + return *x.OperationId + } + return "" +} + +func (x *ChangelogEntry) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +type DiffSummary struct { + state protoimpl.MessageState `protogen:"open.v1"` + Diff bool `protobuf:"varint,1,opt,name=diff,proto3" json:"diff,omitempty"` + Details *DiffDetails `protobuf:"bytes,2,opt,name=details,proto3" json:"details,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DiffSummary) Reset() { + *x = DiffSummary{} + mi := &file_proto_ctrl_v1_openapi_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DiffSummary) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DiffSummary) ProtoMessage() {} + +func (x *DiffSummary) ProtoReflect() protoreflect.Message { + mi := &file_proto_ctrl_v1_openapi_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DiffSummary.ProtoReflect.Descriptor instead. +func (*DiffSummary) Descriptor() ([]byte, []int) { + return file_proto_ctrl_v1_openapi_proto_rawDescGZIP(), []int{2} +} + +func (x *DiffSummary) GetDiff() bool { + if x != nil { + return x.Diff + } + return false +} + +func (x *DiffSummary) GetDetails() *DiffDetails { + if x != nil { + return x.Details + } + return nil +} + +type DiffDetails struct { + state protoimpl.MessageState `protogen:"open.v1"` + Endpoints *DiffCounts `protobuf:"bytes,1,opt,name=endpoints,proto3" json:"endpoints,omitempty"` + Paths *DiffCounts `protobuf:"bytes,2,opt,name=paths,proto3" json:"paths,omitempty"` + Schemas *DiffCounts `protobuf:"bytes,3,opt,name=schemas,proto3" json:"schemas,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DiffDetails) Reset() { + *x = DiffDetails{} + mi := &file_proto_ctrl_v1_openapi_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DiffDetails) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DiffDetails) ProtoMessage() {} + +func (x *DiffDetails) ProtoReflect() protoreflect.Message { + mi := &file_proto_ctrl_v1_openapi_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DiffDetails.ProtoReflect.Descriptor instead. +func (*DiffDetails) Descriptor() ([]byte, []int) { + return file_proto_ctrl_v1_openapi_proto_rawDescGZIP(), []int{3} +} + +func (x *DiffDetails) GetEndpoints() *DiffCounts { + if x != nil { + return x.Endpoints + } + return nil +} + +func (x *DiffDetails) GetPaths() *DiffCounts { + if x != nil { + return x.Paths + } + return nil +} + +func (x *DiffDetails) GetSchemas() *DiffCounts { + if x != nil { + return x.Schemas + } + return nil +} + +type DiffCounts struct { + state protoimpl.MessageState `protogen:"open.v1"` + Added int32 `protobuf:"varint,1,opt,name=added,proto3" json:"added,omitempty"` + Deleted int32 `protobuf:"varint,2,opt,name=deleted,proto3" json:"deleted,omitempty"` + Modified int32 `protobuf:"varint,3,opt,name=modified,proto3" json:"modified,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DiffCounts) Reset() { + *x = DiffCounts{} + mi := &file_proto_ctrl_v1_openapi_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DiffCounts) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DiffCounts) ProtoMessage() {} + +func (x *DiffCounts) ProtoReflect() protoreflect.Message { + mi := &file_proto_ctrl_v1_openapi_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DiffCounts.ProtoReflect.Descriptor instead. +func (*DiffCounts) Descriptor() ([]byte, []int) { + return file_proto_ctrl_v1_openapi_proto_rawDescGZIP(), []int{4} +} + +func (x *DiffCounts) GetAdded() int32 { + if x != nil { + return x.Added + } + return 0 +} + +func (x *DiffCounts) GetDeleted() int32 { + if x != nil { + return x.Deleted + } + return 0 +} + +func (x *DiffCounts) GetModified() int32 { + if x != nil { + return x.Modified + } + return 0 +} + +type GetOpenApiDiffResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Summary *DiffSummary `protobuf:"bytes,1,opt,name=summary,proto3" json:"summary,omitempty"` + HasBreakingChanges bool `protobuf:"varint,2,opt,name=has_breaking_changes,json=hasBreakingChanges,proto3" json:"has_breaking_changes,omitempty"` + Changes []*ChangelogEntry `protobuf:"bytes,3,rep,name=changes,proto3" json:"changes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetOpenApiDiffResponse) Reset() { + *x = GetOpenApiDiffResponse{} + mi := &file_proto_ctrl_v1_openapi_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetOpenApiDiffResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetOpenApiDiffResponse) ProtoMessage() {} + +func (x *GetOpenApiDiffResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_ctrl_v1_openapi_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetOpenApiDiffResponse.ProtoReflect.Descriptor instead. +func (*GetOpenApiDiffResponse) Descriptor() ([]byte, []int) { + return file_proto_ctrl_v1_openapi_proto_rawDescGZIP(), []int{5} +} + +func (x *GetOpenApiDiffResponse) GetSummary() *DiffSummary { + if x != nil { + return x.Summary + } + return nil +} + +func (x *GetOpenApiDiffResponse) GetHasBreakingChanges() bool { + if x != nil { + return x.HasBreakingChanges + } + return false +} + +func (x *GetOpenApiDiffResponse) GetChanges() []*ChangelogEntry { + if x != nil { + return x.Changes + } + return nil +} + +var File_proto_ctrl_v1_openapi_proto protoreflect.FileDescriptor + +const file_proto_ctrl_v1_openapi_proto_rawDesc = "" + + "\n" + + "\x1bproto/ctrl/v1/openapi.proto\x12\actrl.v1\"c\n" + + "\x15GetOpenApiDiffRequest\x12$\n" + + "\x0eold_version_id\x18\x01 \x01(\tR\foldVersionId\x12$\n" + + "\x0enew_version_id\x18\x02 \x01(\tR\fnewVersionId\"\xb5\x01\n" + + "\x0eChangelogEntry\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + + "\x04text\x18\x02 \x01(\tR\x04text\x12\x14\n" + + "\x05level\x18\x03 \x01(\x05R\x05level\x12\x1c\n" + + "\toperation\x18\x04 \x01(\tR\toperation\x12&\n" + + "\foperation_id\x18\x05 \x01(\tH\x00R\voperationId\x88\x01\x01\x12\x12\n" + + "\x04path\x18\x06 \x01(\tR\x04pathB\x0f\n" + + "\r_operation_id\"Q\n" + + "\vDiffSummary\x12\x12\n" + + "\x04diff\x18\x01 \x01(\bR\x04diff\x12.\n" + + "\adetails\x18\x02 \x01(\v2\x14.ctrl.v1.DiffDetailsR\adetails\"\x9a\x01\n" + + "\vDiffDetails\x121\n" + + "\tendpoints\x18\x01 \x01(\v2\x13.ctrl.v1.DiffCountsR\tendpoints\x12)\n" + + "\x05paths\x18\x02 \x01(\v2\x13.ctrl.v1.DiffCountsR\x05paths\x12-\n" + + "\aschemas\x18\x03 \x01(\v2\x13.ctrl.v1.DiffCountsR\aschemas\"X\n" + + "\n" + + "DiffCounts\x12\x14\n" + + "\x05added\x18\x01 \x01(\x05R\x05added\x12\x18\n" + + "\adeleted\x18\x02 \x01(\x05R\adeleted\x12\x1a\n" + + "\bmodified\x18\x03 \x01(\x05R\bmodified\"\xad\x01\n" + + "\x16GetOpenApiDiffResponse\x12.\n" + + "\asummary\x18\x01 \x01(\v2\x14.ctrl.v1.DiffSummaryR\asummary\x120\n" + + "\x14has_breaking_changes\x18\x02 \x01(\bR\x12hasBreakingChanges\x121\n" + + "\achanges\x18\x03 \x03(\v2\x17.ctrl.v1.ChangelogEntryR\achanges2e\n" + + "\x0eOpenApiService\x12S\n" + + "\x0eGetOpenApiDiff\x12\x1e.ctrl.v1.GetOpenApiDiffRequest\x1a\x1f.ctrl.v1.GetOpenApiDiffResponse\"\x00B6Z4github.com/unkeyed/unkey/go/gen/proto/ctrl/v1;ctrlv1b\x06proto3" + +var ( + file_proto_ctrl_v1_openapi_proto_rawDescOnce sync.Once + file_proto_ctrl_v1_openapi_proto_rawDescData []byte +) + +func file_proto_ctrl_v1_openapi_proto_rawDescGZIP() []byte { + file_proto_ctrl_v1_openapi_proto_rawDescOnce.Do(func() { + file_proto_ctrl_v1_openapi_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_ctrl_v1_openapi_proto_rawDesc), len(file_proto_ctrl_v1_openapi_proto_rawDesc))) + }) + return file_proto_ctrl_v1_openapi_proto_rawDescData +} + +var file_proto_ctrl_v1_openapi_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_proto_ctrl_v1_openapi_proto_goTypes = []any{ + (*GetOpenApiDiffRequest)(nil), // 0: ctrl.v1.GetOpenApiDiffRequest + (*ChangelogEntry)(nil), // 1: ctrl.v1.ChangelogEntry + (*DiffSummary)(nil), // 2: ctrl.v1.DiffSummary + (*DiffDetails)(nil), // 3: ctrl.v1.DiffDetails + (*DiffCounts)(nil), // 4: ctrl.v1.DiffCounts + (*GetOpenApiDiffResponse)(nil), // 5: ctrl.v1.GetOpenApiDiffResponse +} +var file_proto_ctrl_v1_openapi_proto_depIdxs = []int32{ + 3, // 0: ctrl.v1.DiffSummary.details:type_name -> ctrl.v1.DiffDetails + 4, // 1: ctrl.v1.DiffDetails.endpoints:type_name -> ctrl.v1.DiffCounts + 4, // 2: ctrl.v1.DiffDetails.paths:type_name -> ctrl.v1.DiffCounts + 4, // 3: ctrl.v1.DiffDetails.schemas:type_name -> ctrl.v1.DiffCounts + 2, // 4: ctrl.v1.GetOpenApiDiffResponse.summary:type_name -> ctrl.v1.DiffSummary + 1, // 5: ctrl.v1.GetOpenApiDiffResponse.changes:type_name -> ctrl.v1.ChangelogEntry + 0, // 6: ctrl.v1.OpenApiService.GetOpenApiDiff:input_type -> ctrl.v1.GetOpenApiDiffRequest + 5, // 7: ctrl.v1.OpenApiService.GetOpenApiDiff:output_type -> ctrl.v1.GetOpenApiDiffResponse + 7, // [7:8] is the sub-list for method output_type + 6, // [6:7] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name +} + +func init() { file_proto_ctrl_v1_openapi_proto_init() } +func file_proto_ctrl_v1_openapi_proto_init() { + if File_proto_ctrl_v1_openapi_proto != nil { + return + } + file_proto_ctrl_v1_openapi_proto_msgTypes[1].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_ctrl_v1_openapi_proto_rawDesc), len(file_proto_ctrl_v1_openapi_proto_rawDesc)), + NumEnums: 0, + NumMessages: 6, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_proto_ctrl_v1_openapi_proto_goTypes, + DependencyIndexes: file_proto_ctrl_v1_openapi_proto_depIdxs, + MessageInfos: file_proto_ctrl_v1_openapi_proto_msgTypes, + }.Build() + File_proto_ctrl_v1_openapi_proto = out.File + file_proto_ctrl_v1_openapi_proto_goTypes = nil + file_proto_ctrl_v1_openapi_proto_depIdxs = nil +} diff --git a/go/go.mod b/go/go.mod index 0fb22cae40..71dcd1bbc8 100644 --- a/go/go.mod +++ b/go/go.mod @@ -10,6 +10,7 @@ require ( github.com/aws/aws-sdk-go-v2/credentials v1.17.70 github.com/aws/aws-sdk-go-v2/service/s3 v1.82.0 github.com/btcsuite/btcutil v1.0.2 + github.com/getkin/kin-openapi v0.132.0 github.com/go-redis/redis/v8 v8.11.5 github.com/go-sql-driver/mysql v1.9.2 github.com/lmittmann/tint v1.1.1 @@ -17,6 +18,7 @@ require ( github.com/oapi-codegen/nullable v1.1.0 github.com/oapi-codegen/oapi-codegen/v2 v2.4.1 github.com/oapi-codegen/runtime v1.1.1 + github.com/oasdiff/oasdiff v1.11.4 github.com/pb33f/libopenapi v0.22.2 github.com/pb33f/libopenapi-validator v0.4.6 github.com/prometheus/client_golang v1.22.0 @@ -46,9 +48,11 @@ require ( require ( cel.dev/expr v0.23.0 // indirect + cloud.google.com/go v0.121.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/ClickHouse/ch-go v0.66.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/TwiN/go-color v1.4.1 // indirect github.com/andybalholm/brotli v1.1.1 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect @@ -80,7 +84,6 @@ require ( github.com/ebitengine/purego v0.8.4 // indirect github.com/fatih/structtag v1.2.0 // indirect github.com/gammazero/deque v1.0.0 // indirect - github.com/getkin/kin-openapi v0.131.0 // indirect github.com/go-faster/city v1.0.1 // indirect github.com/go-faster/errors v0.7.1 // indirect github.com/go-jose/go-jose/v4 v4.0.5 // indirect @@ -133,13 +136,19 @@ require ( github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect github.com/stoewer/go-strcase v1.2.0 // indirect github.com/tetratelabs/wazero v1.8.2 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect github.com/tklauser/go-sysconf v0.3.15 // indirect github.com/tklauser/numcpus v0.10.0 // indirect github.com/unkeyed/unkey/go/deploy/pkg/spiffe v0.0.0-00010101000000-000000000000 // indirect github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect + github.com/wI2L/jsondiff v0.6.1 // indirect github.com/wasilibs/go-pgquery v0.0.0-20240606042535-c0843d6592cc // indirect github.com/wasilibs/wazero-helpers v0.0.0-20240604052452-61d7981e9a38 // indirect github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240815153524-6ea36470d1bd // indirect + github.com/yargevad/filepathx v1.0.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/zeebo/errs v1.4.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect diff --git a/go/go.sum b/go/go.sum index 79b3f7f782..d8bbdc342a 100644 --- a/go/go.sum +++ b/go/go.sum @@ -1,5 +1,7 @@ cel.dev/expr v0.23.0 h1:wUb94w6OYQS4uXraxo9U+wUAs9jT47Xvl4iPgAwM2ss= cel.dev/expr v0.23.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cloud.google.com/go v0.121.0 h1:pgfwva8nGw7vivjZiRfrmglGWiCJBP+0OmDpenG/Fwg= +cloud.google.com/go v0.121.0/go.mod h1:rS7Kytwheu/y9buoDmu5EIpMMCI4Mb8ND4aeN4Vwj7Q= connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= @@ -12,6 +14,8 @@ github.com/ClickHouse/clickhouse-go/v2 v2.35.0/go.mod h1:O2FFT/rugdpGEW2VKyEGyMU github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/TwiN/go-color v1.4.1 h1:mqG0P/KBgHKVqmtL5ye7K0/Gr4l6hTksPgTgMk3mUzc= +github.com/TwiN/go-color v1.4.1/go.mod h1:WcPf/jtiW95WBIsEeY1Lc/b8aaWoiqQpu5cf8WFxu+s= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= @@ -109,8 +113,8 @@ github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/ github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gammazero/deque v1.0.0 h1:LTmimT8H7bXkkCy6gZX7zNLtkbz4NdS2z8LZuor3j34= github.com/gammazero/deque v1.0.0/go.mod h1:iflpYvtGfM3U8S8j+sZEKIak3SAKYpA5/SQewgfXDKo= -github.com/getkin/kin-openapi v0.131.0 h1:NO2UeHnFKRYhZ8wg6Nyh5Cq7dHk4suQQr72a4pMrDxE= -github.com/getkin/kin-openapi v0.131.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58= +github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk= +github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58= github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw= github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw= github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg= @@ -211,6 +215,8 @@ github.com/oapi-codegen/oapi-codegen/v2 v2.4.1 h1:ykgG34472DWey7TSjd8vIfNykXgjOg github.com/oapi-codegen/oapi-codegen/v2 v2.4.1/go.mod h1:N5+lY1tiTDV3V1BeHtOxeWXHoPVeApvsvjJqegfoaz8= github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= +github.com/oasdiff/oasdiff v1.11.4 h1:FgThY78WNwOhWCLIhMk7AsKoHpVZZggRpKEGfd+IOIs= +github.com/oasdiff/oasdiff v1.11.4/go.mod h1:+bDxqI7wMl30OJ97hBfHR5loUsKCctk9UZOujHl8Gtk= github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= @@ -305,7 +311,17 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4= github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= @@ -316,6 +332,8 @@ github.com/urfave/cli/v3 v3.3.3 h1:byCBaVdIXuLPIDm5CYZRVG6NvT7tv1ECqdU4YzlEa3I= github.com/urfave/cli/v3 v3.3.3/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk= github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ= +github.com/wI2L/jsondiff v0.6.1 h1:ISZb9oNWbP64LHnu4AUhsMF5W0FIj5Ok3Krip9Shqpw= +github.com/wI2L/jsondiff v0.6.1/go.mod h1:KAEIojdQq66oJiHhDyQez2x+sRit0vIzC9KeK0yizxM= github.com/wasilibs/go-pgquery v0.0.0-20240606042535-c0843d6592cc h1:Hgim1Xgk1+viV7p0aZh9OOrMRfG+E4mGA+JsI2uB0+k= github.com/wasilibs/go-pgquery v0.0.0-20240606042535-c0843d6592cc/go.mod h1:ah6UfXIl/oA0K3SbourB/UHggVJOBXwPZ2XudDmmFac= github.com/wasilibs/wazero-helpers v0.0.0-20240604052452-61d7981e9a38 h1:RBu75fhabyxyGJ2zhkoNuRyObBMhVeMoXqmeaPTg2CQ= @@ -327,6 +345,8 @@ github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23n github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc= +github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/go/openapi-test b/go/openapi-test new file mode 100755 index 0000000000..2cb1989b7a Binary files /dev/null and b/go/openapi-test differ diff --git a/go/pkg/db/models_generated.go b/go/pkg/db/models_generated.go index 8f5039010f..3d47bb49fe 100644 --- a/go/pkg/db/models_generated.go +++ b/go/pkg/db/models_generated.go @@ -871,6 +871,7 @@ type Version struct { GitCommitSha sql.NullString `db:"git_commit_sha"` GitBranch sql.NullString `db:"git_branch"` ConfigSnapshot json.RawMessage `db:"config_snapshot"` + OpenapiSpec sql.NullString `db:"openapi_spec"` Status VersionsStatus `db:"status"` CreatedAt int64 `db:"created_at"` UpdatedAt sql.NullInt64 `db:"updated_at"` diff --git a/go/pkg/db/querier_generated.go b/go/pkg/db/querier_generated.go index d7acc9cb9a..1da977b49b 100644 --- a/go/pkg/db/querier_generated.go +++ b/go/pkg/db/querier_generated.go @@ -451,6 +451,7 @@ type Querier interface { // git_commit_sha, // git_branch, // config_snapshot, + // openapi_spec, // status, // created_at, // updated_at @@ -907,6 +908,7 @@ type Querier interface { // git_commit_sha, // git_branch, // config_snapshot, + // openapi_spec, // status, // created_at, // updated_at @@ -923,6 +925,7 @@ type Querier interface { // ?, // ?, // ?, + // ?, // ? // ) InsertVersion(ctx context.Context, db DBTX, arg InsertVersionParams) error @@ -1279,6 +1282,12 @@ type Querier interface { // updated_at_m= ? // WHERE id = ? UpdateRatelimitOverride(ctx context.Context, db DBTX, arg UpdateRatelimitOverrideParams) (sql.Result, error) + //UpdateVersionOpenApiSpec + // + // UPDATE versions SET + // openapi_spec = ? + // WHERE id = ? + UpdateVersionOpenApiSpec(ctx context.Context, db DBTX, arg UpdateVersionOpenApiSpecParams) error //UpdateVersionStatus // // UPDATE versions SET diff --git a/go/pkg/db/queries/version_find_by_id.sql b/go/pkg/db/queries/version_find_by_id.sql index 2fc0fe651e..cf148121a6 100644 --- a/go/pkg/db/queries/version_find_by_id.sql +++ b/go/pkg/db/queries/version_find_by_id.sql @@ -9,6 +9,7 @@ SELECT git_commit_sha, git_branch, config_snapshot, + openapi_spec, status, created_at, updated_at diff --git a/go/pkg/db/queries/version_insert.sql b/go/pkg/db/queries/version_insert.sql index 9caf47cb8a..d2730697ac 100644 --- a/go/pkg/db/queries/version_insert.sql +++ b/go/pkg/db/queries/version_insert.sql @@ -9,6 +9,7 @@ INSERT INTO `versions` ( git_commit_sha, git_branch, config_snapshot, + openapi_spec, status, created_at, updated_at @@ -23,6 +24,7 @@ VALUES ( sqlc.arg(git_commit_sha), sqlc.arg(git_branch), sqlc.arg(config_snapshot), + sqlc.arg(openapi_spec), sqlc.arg(status), sqlc.arg(created_at), sqlc.arg(updated_at) diff --git a/go/pkg/db/queries/version_update_openapi_spec.sql b/go/pkg/db/queries/version_update_openapi_spec.sql new file mode 100644 index 0000000000..305186e08d --- /dev/null +++ b/go/pkg/db/queries/version_update_openapi_spec.sql @@ -0,0 +1,4 @@ +-- name: UpdateVersionOpenApiSpec :exec +UPDATE versions SET + openapi_spec = sqlc.arg(openapi_spec) +WHERE id = sqlc.arg(id); \ No newline at end of file diff --git a/go/pkg/db/schema.sql b/go/pkg/db/schema.sql index 6b20e0699b..7a5094271b 100644 --- a/go/pkg/db/schema.sql +++ b/go/pkg/db/schema.sql @@ -384,6 +384,7 @@ CREATE TABLE `versions` ( `git_commit_sha` varchar(40), `git_branch` varchar(256), `config_snapshot` json NOT NULL, + `openapi_spec` text, `status` enum('pending','building','deploying','active','failed','archived') NOT NULL DEFAULT 'pending', `created_at` bigint NOT NULL, `updated_at` bigint, diff --git a/go/pkg/db/version_find_by_id.sql_generated.go b/go/pkg/db/version_find_by_id.sql_generated.go index c26db9b107..56023f1044 100644 --- a/go/pkg/db/version_find_by_id.sql_generated.go +++ b/go/pkg/db/version_find_by_id.sql_generated.go @@ -20,6 +20,7 @@ SELECT git_commit_sha, git_branch, config_snapshot, + openapi_spec, status, created_at, updated_at @@ -39,6 +40,7 @@ WHERE id = ? // git_commit_sha, // git_branch, // config_snapshot, +// openapi_spec, // status, // created_at, // updated_at @@ -57,6 +59,7 @@ func (q *Queries) FindVersionById(ctx context.Context, db DBTX, id string) (Vers &i.GitCommitSha, &i.GitBranch, &i.ConfigSnapshot, + &i.OpenapiSpec, &i.Status, &i.CreatedAt, &i.UpdatedAt, diff --git a/go/pkg/db/version_insert.sql_generated.go b/go/pkg/db/version_insert.sql_generated.go index 652c3208f4..b8efdef2a3 100644 --- a/go/pkg/db/version_insert.sql_generated.go +++ b/go/pkg/db/version_insert.sql_generated.go @@ -22,6 +22,7 @@ INSERT INTO ` + "`" + `versions` + "`" + ` ( git_commit_sha, git_branch, config_snapshot, + openapi_spec, status, created_at, updated_at @@ -38,6 +39,7 @@ VALUES ( ?, ?, ?, + ?, ? ) ` @@ -52,6 +54,7 @@ type InsertVersionParams struct { GitCommitSha sql.NullString `db:"git_commit_sha"` GitBranch sql.NullString `db:"git_branch"` ConfigSnapshot json.RawMessage `db:"config_snapshot"` + OpenapiSpec sql.NullString `db:"openapi_spec"` Status VersionsStatus `db:"status"` CreatedAt int64 `db:"created_at"` UpdatedAt sql.NullInt64 `db:"updated_at"` @@ -69,6 +72,7 @@ type InsertVersionParams struct { // git_commit_sha, // git_branch, // config_snapshot, +// openapi_spec, // status, // created_at, // updated_at @@ -85,6 +89,7 @@ type InsertVersionParams struct { // ?, // ?, // ?, +// ?, // ? // ) func (q *Queries) InsertVersion(ctx context.Context, db DBTX, arg InsertVersionParams) error { @@ -98,6 +103,7 @@ func (q *Queries) InsertVersion(ctx context.Context, db DBTX, arg InsertVersionP arg.GitCommitSha, arg.GitBranch, arg.ConfigSnapshot, + arg.OpenapiSpec, arg.Status, arg.CreatedAt, arg.UpdatedAt, diff --git a/go/pkg/db/version_update_openapi_spec.sql_generated.go b/go/pkg/db/version_update_openapi_spec.sql_generated.go new file mode 100644 index 0000000000..f64c677a9e --- /dev/null +++ b/go/pkg/db/version_update_openapi_spec.sql_generated.go @@ -0,0 +1,32 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: version_update_openapi_spec.sql + +package db + +import ( + "context" + "database/sql" +) + +const updateVersionOpenApiSpec = `-- name: UpdateVersionOpenApiSpec :exec +UPDATE versions SET + openapi_spec = ? +WHERE id = ? +` + +type UpdateVersionOpenApiSpecParams struct { + OpenapiSpec sql.NullString `db:"openapi_spec"` + ID string `db:"id"` +} + +// UpdateVersionOpenApiSpec +// +// UPDATE versions SET +// openapi_spec = ? +// WHERE id = ? +func (q *Queries) UpdateVersionOpenApiSpec(ctx context.Context, db DBTX, arg UpdateVersionOpenApiSpecParams) error { + _, err := db.ExecContext(ctx, updateVersionOpenApiSpec, arg.OpenapiSpec, arg.ID) + return err +} diff --git a/go/proto/ctrl/v1/openapi.proto b/go/proto/ctrl/v1/openapi.proto new file mode 100644 index 0000000000..37225e2029 --- /dev/null +++ b/go/proto/ctrl/v1/openapi.proto @@ -0,0 +1,46 @@ +syntax = "proto3"; + +package ctrl.v1; + +option go_package = "github.com/unkeyed/unkey/go/gen/proto/ctrl/v1;ctrlv1"; + +message GetOpenApiDiffRequest { + string old_version_id = 1; + string new_version_id = 2; +} + +message ChangelogEntry { + string id = 1; + string text = 2; + int32 level = 3; + string operation = 4; + optional string operation_id = 5; + string path = 6; +} + +message DiffSummary { + bool diff = 1; + DiffDetails details = 2; +} + +message DiffDetails { + DiffCounts endpoints = 1; + DiffCounts paths = 2; + DiffCounts schemas = 3; +} + +message DiffCounts { + int32 added = 1; + int32 deleted = 2; + int32 modified = 3; +} + +message GetOpenApiDiffResponse { + DiffSummary summary = 1; + bool has_breaking_changes = 2; + repeated ChangelogEntry changes = 3; +} + +service OpenApiService { + rpc GetOpenApiDiff(GetOpenApiDiffRequest) returns (GetOpenApiDiffResponse) {} +} diff --git a/internal/db/src/schema/projects.ts b/internal/db/src/schema/projects.ts index ae4b1ccdaf..ec7d146c82 100644 --- a/internal/db/src/schema/projects.ts +++ b/internal/db/src/schema/projects.ts @@ -19,6 +19,7 @@ export const projects = mysqlTable( // Git configuration gitRepositoryUrl: varchar("git_repository_url", { length: 500 }), + defaultBranch: varchar("default_branch", { length: 256 }).default("main"), ...deleteProtection, ...lifecycleDates, }, diff --git a/internal/db/src/schema/versions.ts b/internal/db/src/schema/versions.ts index e63d4b8005..ed8bca2298 100644 --- a/internal/db/src/schema/versions.ts +++ b/internal/db/src/schema/versions.ts @@ -1,5 +1,5 @@ import { relations } from "drizzle-orm"; -import { bigint, index, json, mysqlEnum, mysqlTable, varchar } from "drizzle-orm/mysql-core"; +import { bigint, index, json, mysqlEnum, mysqlTable, text, varchar } from "drizzle-orm/mysql-core"; import { branches } from "./branches"; import { builds } from "./builds"; import { projects } from "./projects"; @@ -32,6 +32,9 @@ export const versions = mysqlTable( }>() .notNull(), + // OpenAPI specification + openapiSpec: text("openapi_spec"), + // Version status status: mysqlEnum("status", [ "pending",