Skip to content
Open
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
166 changes: 166 additions & 0 deletions hack/swagger-version-paths/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package main

import (
"encoding/json"
"errors"
"flag"
"fmt"
"os"
"path/filepath"
"strings"

"sigs.k8s.io/yaml"
)

const (
libpodPathPrefix = "/libpod/"
versionedLibpodPathPrefix = "/v{version}/libpod/"
libpodPingPath = "/libpod/_ping"
libpodVersionParameter = "libpodApiVersion"
libpodVersionReference = "#/parameters/" + libpodVersionParameter
)

var libpodVersionDefinition = json.RawMessage(`{
"name": "version",
"in": "path",
"required": true,
"type": "string",
"pattern": "^[0-9][0-9A-Za-z.-]*$",
"description": "Libpod API version. Use the Libpod-API-Version response header from GET /_ping."
}`)

func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

func run() error {
inputPath := flag.String("input", "", "path to the generated Swagger YAML")
outputPath := flag.String("output", "", "path for the transformed Swagger YAML")
flag.Parse()

if *inputPath == "" || *outputPath == "" {
return errors.New("both -input and -output are required")
}
if filepath.Clean(*inputPath) == filepath.Clean(*outputPath) {
return errors.New("-input and -output must be different files")
}

input, err := os.ReadFile(*inputPath)
if err != nil {
return fmt.Errorf("reading generated Swagger: %w", err)
}

output, err := transformSwagger(input)
if err != nil {
return err
}

if err := os.WriteFile(*outputPath, output, 0o644); err != nil {
return fmt.Errorf("writing transformed Swagger: %w", err)
}
return nil
}

func transformSwagger(input []byte) ([]byte, error) {
documentJSON, err := yaml.YAMLToJSON(input)
if err != nil {
return nil, fmt.Errorf("parsing generated Swagger: %w", err)
}

var document map[string]json.RawMessage
if err := json.Unmarshal(documentJSON, &document); err != nil {
return nil, fmt.Errorf("decoding generated Swagger: %w", err)
}

pathsJSON, found := document["paths"]
if !found {
return nil, errors.New("generated Swagger has no paths")
}
var paths map[string]json.RawMessage
if err := json.Unmarshal(pathsJSON, &paths); err != nil {
return nil, fmt.Errorf("decoding Swagger paths: %w", err)
}

transformed := 0
for path, pathItem := range paths {
if !strings.HasPrefix(path, libpodPathPrefix) || path == libpodPingPath {
continue
}

versionedPath := versionedLibpodPathPrefix + strings.TrimPrefix(path, libpodPathPrefix)
if _, exists := paths[versionedPath]; exists {
return nil, fmt.Errorf("versioned Libpod path %q already exists", versionedPath)
}

pathItem, err = addVersionParameter(path, pathItem)
if err != nil {
return nil, err
}

delete(paths, path)
paths[versionedPath] = pathItem
transformed++
}
if transformed == 0 {
return nil, errors.New("generated Swagger has no versioned Libpod paths")
}

parameters := make(map[string]json.RawMessage)
if parametersJSON, found := document["parameters"]; found {
if err := json.Unmarshal(parametersJSON, &parameters); err != nil {
return nil, fmt.Errorf("decoding Swagger parameters: %w", err)
}
}
if _, exists := parameters[libpodVersionParameter]; exists {
return nil, fmt.Errorf("swagger parameter %q already exists", libpodVersionParameter)
}
parameters[libpodVersionParameter] = libpodVersionDefinition

document["paths"], err = json.Marshal(paths)
if err != nil {
return nil, fmt.Errorf("encoding Swagger paths: %w", err)
}
document["parameters"], err = json.Marshal(parameters)
if err != nil {
return nil, fmt.Errorf("encoding Swagger parameters: %w", err)
}

documentJSON, err = json.Marshal(document)
if err != nil {
return nil, fmt.Errorf("encoding transformed Swagger: %w", err)
}
output, err := yaml.JSONToYAML(documentJSON)
if err != nil {
return nil, fmt.Errorf("rendering transformed Swagger: %w", err)
}
return output, nil
}

func addVersionParameter(path string, pathItemJSON json.RawMessage) (json.RawMessage, error) {
var pathItem map[string]json.RawMessage
if err := json.Unmarshal(pathItemJSON, &pathItem); err != nil {
return nil, fmt.Errorf("decoding Swagger path %q: %w", path, err)
}

parameters := make([]json.RawMessage, 0, 1)
if parametersJSON, found := pathItem["parameters"]; found {
if err := json.Unmarshal(parametersJSON, &parameters); err != nil {
return nil, fmt.Errorf("decoding parameters for Swagger path %q: %w", path, err)
}
}
parameters = append(parameters, json.RawMessage(`{"$ref":"`+libpodVersionReference+`"}`))

var err error
pathItem["parameters"], err = json.Marshal(parameters)
if err != nil {
return nil, fmt.Errorf("encoding parameters for Swagger path %q: %w", path, err)
}
output, err := json.Marshal(pathItem)
if err != nil {
return nil, fmt.Errorf("encoding Swagger path %q: %w", path, err)
}
return output, nil
}
154 changes: 154 additions & 0 deletions hack/swagger-version-paths/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package main

import (
"encoding/json"
"strings"
"testing"

"sigs.k8s.io/yaml"
)

func TestTransformSwagger(t *testing.T) {
input := []byte(`
swagger: "2.0"
info:
title: test
version: 6.0.0
parameters:
existing:
name: existing
in: query
type: string
paths:
/info:
get:
responses:
"200":
description: OK
/libpod/_ping:
get:
responses:
"200":
description: OK
/libpod/info:
get:
responses:
"200":
description: OK
/libpod/containers/{name}:
parameters:
- name: name
in: path
required: true
type: string
get:
responses:
"200":
description: OK
`)

output, err := transformSwagger(input)
if err != nil {
t.Fatal(err)
}

document := decodeDocument(t, output)
paths := decodeMap(t, document["paths"])

for _, path := range []string{
"/info",
"/libpod/_ping",
"/v{version}/libpod/info",
"/v{version}/libpod/containers/{name}",
} {
if _, found := paths[path]; !found {
t.Errorf("expected path %q", path)
}
}
for _, path := range []string{"/libpod/info", "/libpod/containers/{name}"} {
if _, found := paths[path]; found {
t.Errorf("unexpected unversioned path %q", path)
}
}

containerPath := decodeMap(t, paths["/v{version}/libpod/containers/{name}"])
var pathParameters []map[string]any
if err := json.Unmarshal(containerPath["parameters"], &pathParameters); err != nil {
t.Fatal(err)
}
if len(pathParameters) != 2 {
t.Fatalf("expected existing and version parameters, got %d", len(pathParameters))
}
if got := pathParameters[1]["$ref"]; got != libpodVersionReference {
t.Errorf("expected version parameter reference %q, got %q", libpodVersionReference, got)
}

parameters := decodeMap(t, document["parameters"])
if _, found := parameters["existing"]; !found {
t.Error("existing global parameter was removed")
}
var versionParameter map[string]any
if err := json.Unmarshal(parameters[libpodVersionParameter], &versionParameter); err != nil {
t.Fatal(err)
}
if got := versionParameter["name"]; got != "version" {
t.Errorf("expected path parameter name version, got %q", got)
}
if got := versionParameter["required"]; got != true {
t.Errorf("expected required path parameter, got %v", got)
}
}

func TestTransformSwaggerRejectsPathCollision(t *testing.T) {
input := []byte(`
swagger: "2.0"
paths:
/libpod/info:
get: {}
/v{version}/libpod/info:
get: {}
`)

_, err := transformSwagger(input)
if err == nil || !strings.Contains(err.Error(), "already exists") {
t.Fatalf("expected path collision error, got %v", err)
}
}

func TestTransformSwaggerRequiresLibpodPaths(t *testing.T) {
input := []byte(`
swagger: "2.0"
paths:
/info:
get: {}
`)

_, err := transformSwagger(input)
if err == nil || !strings.Contains(err.Error(), "no versioned Libpod paths") {
t.Fatalf("expected missing Libpod paths error, got %v", err)
}
}

func decodeDocument(t *testing.T, input []byte) map[string]json.RawMessage {
t.Helper()

documentJSON, err := yaml.YAMLToJSON(input)
if err != nil {
t.Fatal(err)
}
var document map[string]json.RawMessage
if err := json.Unmarshal(documentJSON, &document); err != nil {
t.Fatal(err)
}
return document
}

func decodeMap(t *testing.T, input json.RawMessage) map[string]json.RawMessage {
t.Helper()

var output map[string]json.RawMessage
if err := json.Unmarshal(input, &output); err != nil {
t.Fatal(err)
}
return output
}
7 changes: 5 additions & 2 deletions pkg/api/Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
SWAGGER_OUT ?= swagger.yaml
SWAGGER_TMP = ${SWAGGER_OUT}.tmp.yaml

SWAGGER ?= ../../test/tools/build/swagger

Expand All @@ -11,5 +12,7 @@ serve: ${SWAGGER_OUT}
.PHONY: ${SWAGGER_OUT}
${SWAGGER_OUT}:
# generate doesn't remove file on error
rm -f ${SWAGGER_OUT}
$(SWAGGER) generate spec -c go.podman.io/podman -x github.com/sigstore/rekor -x github.com/moby/moby/api/types/network -x github.com/docker/docker/api/types/network -x github.com/moby/moby/api -o ${SWAGGER_OUT} -i tags.yaml -w ./ -m
rm -f ${SWAGGER_OUT} ${SWAGGER_TMP}
$(SWAGGER) generate spec -c go.podman.io/podman -x github.com/sigstore/rekor -x github.com/moby/moby/api/types/network -x github.com/docker/docker/api/types/network -x github.com/moby/moby/api -o ${SWAGGER_TMP} -i tags.yaml -w ./ -m
go run ../../hack/swagger-version-paths -input ${SWAGGER_TMP} -output ${SWAGGER_OUT}
rm -f ${SWAGGER_TMP}