Skip to content

Commit

Permalink
fix(kyaml): add lock for schema related globals
Browse files Browse the repository at this point in the history
  • Loading branch information
pmalek committed Jan 24, 2023
1 parent 659c0ee commit 43de3d9
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 13 deletions.
2 changes: 1 addition & 1 deletion kyaml/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ lint: $(MYGOBIN)/golangci-lint
--skip-dirs internal/forked

test:
go test -v -cover ./...
go test -race -v -cover ./...

fix:
go fix ./...
Expand Down
44 changes: 35 additions & 9 deletions kyaml/openapi/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"path/filepath"
"reflect"
"strings"
"sync"

openapi_v2 "github.com/google/gnostic/openapiv2"
"google.golang.org/protobuf/proto"
Expand All @@ -21,14 +22,27 @@ import (
k8syaml "sigs.k8s.io/yaml"
)

// globalSchema contains global state information about the openapi
var globalSchema openapiData

// kubernetesOpenAPIVersion specifies which builtin kubernetes schema to use
var kubernetesOpenAPIVersion string

// customSchemaFile stores the custom OpenApi schema if it is provided
var customSchema []byte
var (
// schemaLock is the lock for schema related globals.
//
// NOTE: This lock helps with preventing panics that might occur due to the data
// race that concurrent access on this variable might cause but it doesn't
// fully fix the issue described in https://github.com/kubernetes-sigs/kustomize/issues/4824.
// For instance concurrently running goroutines where each of them calls SetSchema()
// and/or GetSchemaVersion might end up received nil errors (success) whereas the
// seconds one would overwrite the global variable that has been written by the
// first one.
schemaLock sync.RWMutex //nolint:gochecknoglobals

// kubernetesOpenAPIVersion specifies which builtin kubernetes schema to use.
kubernetesOpenAPIVersion string //nolint:gochecknoglobals

// globalSchema contains global state information about the openapi
globalSchema openapiData //nolint:gochecknoglobals

// customSchemaFile stores the custom OpenApi schema if it is provided
customSchema []byte //nolint:gochecknoglobals
)

// openapiData contains the parsed openapi state. this is in a struct rather than
// a list of vars so that it can be reset from tests.
Expand Down Expand Up @@ -278,9 +292,12 @@ func AddSchema(s []byte) error {

// ResetOpenAPI resets the openapi data to empty
func ResetOpenAPI() {
schemaLock.Lock()
defer schemaLock.Unlock()

globalSchema = openapiData{}
kubernetesOpenAPIVersion = ""
customSchema = nil
kubernetesOpenAPIVersion = ""
}

// AddDefinitions adds the definitions to the global schema.
Expand Down Expand Up @@ -551,6 +568,9 @@ const (

// SetSchema sets the kubernetes OpenAPI schema version to use
func SetSchema(openAPIField map[string]string, schema []byte, reset bool) error {
schemaLock.Lock()
defer schemaLock.Unlock()

// this should only be set once
schemaIsSet := (kubernetesOpenAPIVersion != "") || customSchema != nil
if schemaIsSet && !reset {
Expand Down Expand Up @@ -588,6 +608,9 @@ func SetSchema(openAPIField map[string]string, schema []byte, reset bool) error

// GetSchemaVersion returns what kubernetes OpenAPI version is being used
func GetSchemaVersion() string {
schemaLock.RLock()
defer schemaLock.RUnlock()

switch {
case kubernetesOpenAPIVersion == "" && customSchema == nil:
return kubernetesOpenAPIDefaultVersion
Expand All @@ -600,6 +623,9 @@ func GetSchemaVersion() string {

// initSchema parses the json schema
func initSchema() {
schemaLock.Lock()
defer schemaLock.Unlock()

if globalSchema.schemaInit {
return
}
Expand Down
74 changes: 71 additions & 3 deletions kyaml/openapi/openapi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ package openapi
import (
"fmt"
"os"
"sync"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sigs.k8s.io/kustomize/kyaml/yaml"
)

Expand Down Expand Up @@ -120,7 +122,7 @@ openAPI:
if !assert.NoError(t, err) {
t.FailNow()
}
if !assert.NoError(t, os.WriteFile(f.Name(), []byte(inputyaml), 0600)) {
if !assert.NoError(t, os.WriteFile(f.Name(), []byte(inputyaml), 0o600)) {
t.FailNow()
}

Expand Down Expand Up @@ -172,7 +174,7 @@ openAPI:
if !assert.NoError(t, err) {
t.FailNow()
}
if !assert.NoError(t, os.WriteFile(f.Name(), []byte(inputyaml), 0600)) {
if !assert.NoError(t, os.WriteFile(f.Name(), []byte(inputyaml), 0o600)) {
t.FailNow()
}

Expand Down Expand Up @@ -207,7 +209,7 @@ kind: Example
if !assert.NoError(t, err) {
t.FailNow()
}
if !assert.NoError(t, os.WriteFile(f.Name(), []byte(inputyaml), 0600)) {
if !assert.NoError(t, os.WriteFile(f.Name(), []byte(inputyaml), 0o600)) {
t.FailNow()
}

Expand Down Expand Up @@ -325,3 +327,69 @@ func TestIsNamespaceScoped_custom(t *testing.T) {
assert.True(t, isFound)
assert.True(t, isNamespaceable)
}

func TestCanSetAndResetSchemaConcurrently(t *testing.T) {
t.Run("SetSchema doesn't cause a data race when called concurrently", func(t *testing.T) {
set := func(wg *sync.WaitGroup) {
defer wg.Done()
err := SetSchema(
map[string]string{
"/apis/custom.io/v1": "true",
},
[]byte(`
{
"definitions": {},
"paths": {
"/apis/custom.io/v1/namespaces/{namespace}/customs/{name}": {
"get": {
"x-kubernetes-action": "get",
"x-kubernetes-group-version-kind": {
"group": "custom.io",
"kind": "Custom",
"version": "v1"
}
}
},
"/apis/custom.io/v1/clustercustoms": {
"get": {
"x-kubernetes-action": "get",
"x-kubernetes-group-version-kind": {
"group": "custom.io",
"kind": "ClusterCustom",
"version": "v1"
}
}
}
}
}
`),
true,
)
require.NoError(t, err)
}

var wg sync.WaitGroup
require.NotPanics(t, func() {
for i := 0; i < 100; i++ {
wg.Add(1)
go set(&wg)
}
})
wg.Wait()
})

t.Run("ResetOpenAPI doesn't cause a data race when called concurrently", func(t *testing.T) {
reset := func(wg *sync.WaitGroup) {
defer wg.Done()
ResetOpenAPI()
}
var wg sync.WaitGroup
require.NotPanics(t, func() {
for i := 0; i < 100; i++ {
wg.Add(1)
go reset(&wg)
}
})
wg.Wait()
})
}

0 comments on commit 43de3d9

Please sign in to comment.