Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor fake versions of openapi client into testing subdir #115465

Merged
merged 1 commit into from Feb 15, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
131 changes: 131 additions & 0 deletions staging/src/k8s.io/client-go/openapi/openapitest/fake.go
@@ -0,0 +1,131 @@
/*
Copyright 2023 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package openapitest
seans3 marked this conversation as resolved.
Show resolved Hide resolved

import (
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"

"k8s.io/client-go/openapi"
)

// NewFileClient returns a pointer to a testing
// FileOpenAPIClient, which parses the OpenAPI GroupVersion
// files in the passed directory path. Example GroupVersion
// OpenAPI filename for apps/v1 would look like:
//
// apis__apps__v1_openapi.json
//
// The current directory housing these hard-coded GroupVersion
// OpenAPI V3 specification files is:
//
// <K8S_ROOT>/api/openapi-spec/v3
//
// An example to invoke this function for the test files:
//
// NewFileClient("../../../api/openapi-spec/v3")
//
// This function will search passed directory for files
// with the suffix "_openapi.json". IMPORTANT: If any file in
// the directory does NOT parse correctly, this function will
// panic.
func NewFileClient(fullDirPath string) openapi.Client {
_, err := os.Stat(fullDirPath)
if err != nil {
panic(fmt.Sprintf("Unable to find test file directory: %s\n", fullDirPath))
}
files, err := ioutil.ReadDir(fullDirPath)
if err != nil {
panic(fmt.Sprintf("Error reading test file directory: %s (%s)\n", err, fullDirPath))
}
values := map[string]openapi.GroupVersion{}
for _, fileInfo := range files {
filename := fileInfo.Name()
apiFilename, err := apiFilepath(filename)
if err != nil {
panic(fmt.Sprintf("Error translating file to apipath: %s (%s)\n", err, filename))
}
fullFilename := filepath.Join(fullDirPath, filename)
gvFile := fileOpenAPIGroupVersion{filepath: fullFilename}
values[apiFilename] = gvFile
}
return &fileOpenAPIClient{values: values}
}

// fileOpenAPIClient is a testing version implementing the
// openapi.Client interface. This struct stores the hard-coded
// values returned by this file client.
type fileOpenAPIClient struct {
values map[string]openapi.GroupVersion
}

// fileOpenAPIClient implements the openapi.Client interface.
var _ openapi.Client = &fileOpenAPIClient{}

// Paths returns the hard-coded map of the api server relative URL
// path string to the GroupVersion swagger bytes. An example Path
// string for apps/v1 GroupVersion is:
//
// apis/apps/v1
func (f fileOpenAPIClient) Paths() (map[string]openapi.GroupVersion, error) {
return f.values, nil
}

// fileOpenAPIGroupVersion is a testing version implementing the
// openapi.GroupVersion interface. This struct stores the full
// filepath to the file storing the hard-coded GroupVersion bytes.
type fileOpenAPIGroupVersion struct {
filepath string
}

// FileOpenAPIGroupVersion implements the openapi.GroupVersion interface.
var _ openapi.GroupVersion = &fileOpenAPIGroupVersion{}

// Schemas returns the GroupVersion bytes at the stored filepath, or
// an error if one is returned from reading the file. Panics if the
// passed contentType string is not "application/json".
func (f fileOpenAPIGroupVersion) Schema(contentType string) ([]byte, error) {
if contentType != "application/json" {
panic("FileOpenAPI only supports 'application/json' contentType")
}
return ioutil.ReadFile(f.filepath)
apelisse marked this conversation as resolved.
Show resolved Hide resolved
}

// apiFilepath is a helper function to parse a openapi filename
// and transform it to the corresponding api relative url. This function
// is the inverse of the filenaming for OpenAPI V3 specs in the
// hack/update-openapi-spec.sh
//
// Examples:
//
// apis__apps__v1_openapi.json -> apis/apps/v1
// apis__networking.k8s.io__v1alpha1_openapi.json -> apis/networking.k8s.io/v1alpha1
// api__v1_openapi.json -> api/v1
// logs_openapi.json -> logs
func apiFilepath(filename string) (string, error) {
apelisse marked this conversation as resolved.
Show resolved Hide resolved
if !strings.HasSuffix(filename, "_openapi.json") {
errStr := fmt.Sprintf("Unable to parse openapi v3 spec filename: %s", filename)
return "", errors.New(errStr)
}
filename = strings.TrimSuffix(filename, "_openapi.json")
filepath := strings.ReplaceAll(filename, "__", "/")
return filepath, nil
}
168 changes: 168 additions & 0 deletions staging/src/k8s.io/client-go/openapi/openapitest/fake_test.go
@@ -0,0 +1,168 @@
/*
Copyright 2023 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package openapitest

import (
"io/ioutil"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

var openAPIV3SpecDir = "../../../../../../api/openapi-spec/v3"

func TestFileOpenAPIClient_Paths(t *testing.T) {
// Directory with OpenAPI V3 spec files
tests := map[string]struct {
path string
found bool
}{
"apps/v1 path exists": {
path: "apis/apps/v1",
found: true,
},
"core/v1 path exists": {
path: "api/v1",
found: true,
},
"batch/v1 path exists": {
path: "apis/batch/v1",
found: true,
},
"networking/v1alpha1 path exists": {
path: "apis/networking.k8s.io/v1alpha1",
found: true,
},
"discovery/v1 path exists": {
path: "apis/discovery.k8s.io/v1",
found: true,
},
"fake path does not exists": {
path: "does/not/exist",
found: false,
},
}

fileClient := NewFileClient(openAPIV3SpecDir)
paths, err := fileClient.Paths()
require.NoError(t, err)
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
_, found := paths[tc.path]
if tc.found {
require.True(t, found)
} else {
require.False(t, found)
}
})
}
}

func TestFileOpenAPIClient_GroupVersions(t *testing.T) {
tests := map[string]struct {
path string
filename string
}{
"apps/v1 groupversion spec validation": {
path: "apis/apps/v1",
filename: "apis__apps__v1_openapi.json",
},
"core/v1 groupversion spec validation": {
path: "api/v1",
filename: "api__v1_openapi.json",
},
"networking/v1alpha1 groupversion spec validation": {
path: "apis/networking.k8s.io/v1alpha1",
filename: "apis__networking.k8s.io__v1alpha1_openapi.json",
},
}

fileClient := NewFileClient(openAPIV3SpecDir)
paths, err := fileClient.Paths()
require.NoError(t, err)
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
gv, found := paths[tc.path]
require.True(t, found)
actualBytes, err := gv.Schema("application/json")
require.NoError(t, err)
expectedBytes, err := ioutil.ReadFile(
filepath.Join(openAPIV3SpecDir, tc.filename))
require.NoError(t, err)
assert.Equal(t, expectedBytes, actualBytes)
})
}
}

func TestFileOpenAPIClient_apiFilePath(t *testing.T) {
apelisse marked this conversation as resolved.
Show resolved Hide resolved
tests := map[string]struct {
filename string
expected string
isError bool
}{
"apps/v1 filename": {
filename: "apis__apps__v1_openapi.json",
expected: "apis/apps/v1",
},
"core/v1 filename": {
filename: "api__v1_openapi.json",
expected: "api/v1",
},
"logs filename": {
filename: "logs_openapi.json",
expected: "logs",
},
"api filename": {
filename: "api_openapi.json",
expected: "api",
},
"unversioned autoscaling filename": {
filename: "apis__autoscaling_openapi.json",
expected: "apis/autoscaling",
},
"networking/v1alpha1 filename": {
filename: "apis__networking.k8s.io__v1alpha1_openapi.json",
expected: "apis/networking.k8s.io/v1alpha1",
},
"batch/v1beta1 filename": {
filename: "apis__batch__v1beta1_openapi.json",
expected: "apis/batch/v1beta1",
},
"non-JSON suffix is invalid": {
filename: "apis__networking.k8s.io__v1alpha1_openapi.yaml",
isError: true,
},
"missing final openapi before suffix is invalid": {
filename: "apis__apps__v1_something.json",
isError: true,
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
actual, err := apiFilepath(tc.filename)
if !tc.isError {
require.NoError(t, err)
assert.Equal(t, tc.expected, actual)
} else {
require.Error(t, err)
}
})
}
}