336 changes: 166 additions & 170 deletions api/server/middleware_auth.go

Large diffs are not rendered by default.

1,388 changes: 1,388 additions & 0 deletions api/server/mock/mock_schedops_k8s.go

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions api/server/sdk/volume_ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (

"github.com/libopenstorage/openstorage/api"
"github.com/libopenstorage/openstorage/pkg/auth"
"github.com/libopenstorage/openstorage/pkg/auth/secrets"
policy "github.com/libopenstorage/openstorage/pkg/storagepolicy"
"github.com/libopenstorage/openstorage/pkg/util"
"github.com/libopenstorage/openstorage/volume"
Expand All @@ -33,6 +34,17 @@ import (
"google.golang.org/grpc/status"
)

var (
// AdminOwnedLabelKeys is a set of labels that only the storage admin
// can change.
AdminOwnedLabelKeys = []string{
secrets.SecretNameKey,
secrets.SecretNamespaceKey,
api.KubernetesPvcNameKey,
api.KubernetesPvcNamespaceKey,
}
)

// When create is called for an existing volume, this function is called to make sure
// the SDK only returns that the volume is ready when the status is UP
func (s *VolumeServer) waitForVolumeReady(ctx context.Context, id string) (*api.Volume, error) {
Expand Down Expand Up @@ -563,6 +575,15 @@ func (s *VolumeServer) Update(
return nil, err
}

// Only the administrator can change admin-only labels
if !api.IsAdminByContext(ctx) && req.GetLabels() != nil {
for _, adminKey := range AdminOwnedLabelKeys {
if _, ok := req.GetLabels()[adminKey]; ok {
return nil, status.Errorf(codes.PermissionDenied, "Only the administrator can update label %s", adminKey)
}
}
}

// Check if the caller can update the volume
if !resp.GetVolume().IsPermitted(ctx, api.Ownership_Write) {
return nil, status.Errorf(codes.PermissionDenied, "Cannot update volume")
Expand Down
74 changes: 74 additions & 0 deletions api/server/sdk/volume_ops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,80 @@ func TestSdkVolumeEnumerateWithFilters(t *testing.T) {
assert.Equal(t, r.GetVolumeIds()[0], id)
}

func TestSdkVolumeUpdateAdminLabels(t *testing.T) {
// This test does not use the gRPC server
mc := gomock.NewController(&utils.SafeGoroutineTester{})
mv := mockdriver.NewMockVolumeDriver(mc)
mcluster := mockcluster.NewMockCluster(mc)

// Setup server
s := VolumeServer{
server: &sdkGrpcServer{
// This will enable isAuthEnabled to return true
config: ServerConfig{
Security: &SecurityConfig{
Authenticators: map[string]auth.Authenticator{
"hello": nil,
"another": nil,
},
},
},
driverHandlers: map[string]volume.VolumeDriver{
"mock": mv,
DefaultDriverName: mv,
},
clusterHandler: mcluster,
},
}

id := "myid"
newlabels := map[string]string{
api.KubernetesPvcNameKey: "hello",
}
req := &api.SdkVolumeUpdateRequest{
VolumeId: id,
Labels: newlabels,
}

// Check Locator
mv.
EXPECT().
Enumerate(&api.VolumeLocator{
VolumeIds: []string{id},
}, nil).
Return([]*api.Volume{&api.Volume{Spec: &api.VolumeSpec{}}}, nil).
AnyTimes()
mv.
EXPECT().
Set(gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil).
AnyTimes()

// m
ctxNoAuth := context.Background()
ctxNotAdmin := auth.ContextSaveUserInfo(context.Background(), &auth.UserInfo{
Username: "notmyname",
})
ctxAdmin := auth.ContextSaveUserInfo(context.Background(), &auth.UserInfo{
Username: "admin",
Claims: auth.Claims{
Groups: []string{"*"},
},
})

// No auth enabled
_, err := s.Update(ctxNoAuth, req)
assert.NoError(t, err)

// Ctx has auth but not admin
_, err = s.Update(ctxNotAdmin, req)
assert.Error(t, err)

// Ctx has auth but not admin
_, err = s.Update(ctxAdmin, req)
assert.NoError(t, err)
}

func TestSdkVolumeUpdate(t *testing.T) {

// Create server and client connection
Expand Down
35 changes: 27 additions & 8 deletions api/server/testutils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/kubernetes-csi/csi-test/utils"
"github.com/libopenstorage/openstorage/api"
mockapi "github.com/libopenstorage/openstorage/api/mock"
servermock "github.com/libopenstorage/openstorage/api/server/mock"
"github.com/libopenstorage/openstorage/api/server/sdk"
"github.com/libopenstorage/openstorage/cluster"
clustermanager "github.com/libopenstorage/openstorage/cluster/manager"
Expand All @@ -42,6 +43,8 @@ import (

"google.golang.org/grpc"
"google.golang.org/grpc/metadata"

schedopsk8s "github.com/portworx/sched-ops/k8s/core"
)

const (
Expand All @@ -63,14 +66,16 @@ var (
// testServer is a simple struct used abstract
// the creation and setup of the gRPC CSI service and REST server
type testServer struct {
conn *grpc.ClientConn
m *mockdriver.MockVolumeDriver
c cluster.Cluster
s *mockapi.MockOpenStoragePoolServer
mc *gomock.Controller
sdk *sdk.Server
port string
gwport string
conn *grpc.ClientConn
m *mockdriver.MockVolumeDriver
c cluster.Cluster
s *mockapi.MockOpenStoragePoolServer
k8sops *servermock.MockOps
originalOps schedopsk8s.Ops
mc *gomock.Controller
sdk *sdk.Server
port string
gwport string
}

// Struct used for creation and setup of cluster api testing
Expand Down Expand Up @@ -138,6 +143,10 @@ func newTestServerSdkNoAuth(t *testing.T) *testServer {
tester.m = mockdriver.NewMockVolumeDriver(tester.mc)
tester.c = mockcluster.NewMockCluster(tester.mc)
tester.s = mockapi.NewMockOpenStoragePoolServer(tester.mc)
tester.k8sops = servermock.NewMockOps(tester.mc)

tester.originalOps = schedopsk8s.Instance()
schedopsk8s.SetInstance(tester.k8sops)

kv, err := kvdb.New(mem.Name, "test", []string{}, nil, kvdb.LogFatalErrorCB)
assert.NoError(t, err)
Expand Down Expand Up @@ -194,6 +203,10 @@ func newTestServerSdk(t *testing.T) *testServer {
tester.m = mockdriver.NewMockVolumeDriver(tester.mc)
tester.c = mockcluster.NewMockCluster(tester.mc)
tester.s = mockapi.NewMockOpenStoragePoolServer(tester.mc)
tester.k8sops = servermock.NewMockOps(tester.mc)

tester.originalOps = schedopsk8s.Instance()
schedopsk8s.SetInstance(tester.k8sops)

// Create a role manager
kv, err := kvdb.New(mem.Name, "test", []string{}, nil, kvdb.LogFatalErrorCB)
Expand Down Expand Up @@ -308,6 +321,10 @@ func (s *testServer) MockDriver() *mockdriver.MockVolumeDriver {
return s.m
}

func (s *testServer) MockK8sOps() *servermock.MockOps {
return s.k8sops
}

func (s *testServer) Conn() *grpc.ClientConn {
return s.conn
}
Expand Down Expand Up @@ -416,6 +433,8 @@ func (s *testServer) Stop() {

// Remove from registry
volumedrivers.Remove(mockDriverName)

schedopsk8s.SetInstance(s.originalOps)
}

func createToken(name, role, secret string) (string, error) {
Expand Down
32 changes: 29 additions & 3 deletions api/server/volume.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/libopenstorage/openstorage/api/server/sdk"
clustermanager "github.com/libopenstorage/openstorage/cluster/manager"
"github.com/libopenstorage/openstorage/pkg/auth"
"github.com/libopenstorage/openstorage/pkg/auth/secrets"
"github.com/libopenstorage/openstorage/pkg/grpcserver"
"github.com/libopenstorage/openstorage/pkg/options"
"github.com/libopenstorage/openstorage/volume"
Expand Down Expand Up @@ -194,6 +195,12 @@ func (vd *volAPI) create(w http.ResponseWriter, r *http.Request) {
if err := json.NewDecoder(r.Body).Decode(&dcReq); err != nil {
fmt.Println("returning error here")
vd.sendError(vd.name, method, w, err.Error(), http.StatusBadRequest)
}
if dcReq.GetSpec() == nil {
vd.sendError(vd.name, method, w, "Must supply a volume specification", http.StatusBadRequest)
return
} else if dcReq.GetLocator() == nil {
vd.sendError(vd.name, method, w, "Must supply a volume locator", http.StatusBadRequest)
return
}

Expand All @@ -204,6 +211,26 @@ func (vd *volAPI) create(w http.ResponseWriter, r *http.Request) {
return
}

// Check headers for secret reference. These are set by the Kubernetes auth middleware
secretName := r.Header.Get(secrets.SecretNameKey)
secretNamespace := r.Header.Get(secrets.SecretNamespaceKey)
pvcName := r.Header.Get(api.KubernetesPvcNameKey)
pvcNamespace := r.Header.Get(api.KubernetesPvcNamespaceKey)
if len(secretName) != 0 && len(secretNamespace) != 0 {
if dcReq.GetLocator().GetVolumeLabels() == nil {
dcReq.GetLocator().VolumeLabels = make(map[string]string)
}
dcReq.GetLocator().GetVolumeLabels()[secrets.SecretNameKey] = secretName
dcReq.GetLocator().GetVolumeLabels()[secrets.SecretNamespaceKey] = secretNamespace

// Only add the pvc name and namespace if we had the secrets and if the
// pvc values where passed
if len(pvcName) != 0 && len(pvcNamespace) != 0 {
dcReq.GetLocator().GetVolumeLabels()[api.KubernetesPvcNameKey] = pvcName
dcReq.GetLocator().GetVolumeLabels()[api.KubernetesPvcNamespaceKey] = pvcNamespace
}
}

// Get gRPC connection
conn, err := vd.getConn()
if err != nil {
Expand Down Expand Up @@ -452,7 +479,6 @@ func (vd *volAPI) volumeSet(w http.ResponseWriter, r *http.Request) {
}
}
json.NewEncoder(w).Encode(resp)

}

func getVolumeUpdateSpec(spec *api.VolumeSpec, vol *api.Volume) *api.VolumeSpecUpdate {
Expand Down Expand Up @@ -1822,14 +1848,14 @@ func (vd *volAPI) SetupRoutesWithAuth(
nInspect := negroni.New()
nInspect.Use(negroni.HandlerFunc(authM.inspectWithAuth))
inspectRoute := vd.volumeInspectRoute()
nSet.UseHandlerFunc(inspectRoute.fn)
nInspect.UseHandlerFunc(inspectRoute.fn)
router.Methods(inspectRoute.verb).Path(inspectRoute.path).Handler(nInspect)

// Setup middleware for enumerate
nEnumerate := negroni.New()
nEnumerate.Use(negroni.HandlerFunc(authM.enumerateWithAuth))
enumerateRoute := vd.volumeEnumerateRoute()
nSet.UseHandlerFunc(enumerateRoute.fn)
nEnumerate.UseHandlerFunc(enumerateRoute.fn)
router.Methods(enumerateRoute.verb).Path(enumerateRoute.path).Handler(nEnumerate)

routes := []*Route{vd.versionRoute()}
Expand Down
141 changes: 97 additions & 44 deletions api/server/volume_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,18 @@ import (
"github.com/stretchr/testify/assert"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

"github.com/sirupsen/logrus"

corev1 "k8s.io/api/core/v1"
storagev1 "k8s.io/api/storage/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func init() {
logrus.SetLevel(logrus.PanicLevel)
}

func TestVolumeNoAuth(t *testing.T) {
var err error

Expand Down Expand Up @@ -189,7 +199,8 @@ func TestMiddlewareVolumeCreateFailure(t *testing.T) {
size := uint64(1234)
secretName := "secret-name"
namespace := "ns"
tokenKey := "token-key"
pvcName := "mypvc"
storageClassName := "storageclass1"

req := &api.VolumeCreateRequest{
Locator: &api.VolumeLocator{
Expand Down Expand Up @@ -218,13 +229,32 @@ func TestMiddlewareVolumeCreateFailure(t *testing.T) {
_, err = driverclient.Create(req.GetLocator(), req.GetSource(), req.GetSpec())
assert.Error(t, err, "Expected an error on Create")

pvc := corev1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: pvcName,
Namespace: namespace,
},
Spec: corev1.PersistentVolumeClaimSpec{
StorageClassName: &storageClassName,
},
}

storageClass := storagev1.StorageClass{
ObjectMeta: metav1.ObjectMeta{
Name: storageClassName,
},
Parameters: map[string]string{
secrets.SecretNameKey: secretName,
secrets.SecretNamespaceKey: "${pvc.namespace}",
},
}

req = &api.VolumeCreateRequest{
Locator: &api.VolumeLocator{
Name: name,
VolumeLabels: map[string]string{
secrets.SecretNameKey: secretName,
secrets.SecretTokenKey: tokenKey,
secrets.SecretNamespaceKey: namespace,
PVCNameLabelKey: pvcName,
PVCNamespaceLabelKey: namespace,
},
},
Source: &api.Source{},
Expand All @@ -236,6 +266,11 @@ func TestMiddlewareVolumeCreateFailure(t *testing.T) {
},
}

testVolDriver.MockK8sOps().EXPECT().
GetPersistentVolumeClaim(pvcName, namespace).Return(&pvc, nil).AnyTimes()
testVolDriver.MockK8sOps().EXPECT().
GetStorageClassForPVC(&pvc).Return(&storageClass, nil).AnyTimes()

// Send a request and fail to get a token
mockSecret.EXPECT().
GetSecret(
Expand Down Expand Up @@ -873,38 +908,6 @@ func TestMiddlewareVolumeSetSizeSuccess(t *testing.T) {
assert.NoError(t, err)
}

func TestMiddlewareVolumeSetFailure(t *testing.T) {
testVolDriver := newTestServerSdk(t)
defer testVolDriver.Stop()

_, mockSecret, mc := getSecretsMock(t)
defer mc.Finish()
lsecrets.SetInstance(mockSecret)

// TODO(stgleb): Fix it
unixServer, portServer, err := StartVolumeMgmtAPI(fakeWithSched, testSdkSock, testMgmtBase, testMgmtPort, true, nil)
assert.NoError(t, err, "Unexpected error on StartVolumeMgmtAPI")
defer unixServer.Close()
defer portServer.Close()

time.Sleep(1 * time.Second)
c, err := volumeclient.NewDriverClient(testMockURL, fakeWithSched, version, fakeWithSched)
assert.NoError(t, err, "Unexpected error on NewDriverClient")

driverclient := volumeclient.VolumeDriver(c)
id, _, _, _ := testMiddlewareCreateVolume(t, driverclient, mockSecret, testVolDriver)

req := &api.VolumeSetRequest{
Spec: &api.VolumeSpec{Shared: true},
}

// Not setting mock secrets

err = driverclient.Set(id, &api.VolumeLocator{Name: "myvol"}, req.GetSpec())
assert.Error(t, err, "Unexpected error on Set")

}

func TestVolumeAttachSuccess(t *testing.T) {

var err error
Expand Down Expand Up @@ -2438,18 +2441,38 @@ func TestMiddlewareVolumeDeleteFailureIncorrectToken(t *testing.T) {
size := uint64(1234)
secretName := "secret-name"
namespace := "ns"
tokenKey := "token-key"
pvcName := "mypvc"
storageClassName := "storageclass1"
// get token
token, err := createToken("test", "system.admin", testSharedSecret)
assert.NoError(t, err)

pvc := corev1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: pvcName,
Namespace: namespace,
},
Spec: corev1.PersistentVolumeClaimSpec{
StorageClassName: &storageClassName,
},
}

storageClass := storagev1.StorageClass{
ObjectMeta: metav1.ObjectMeta{
Name: storageClassName,
},
Parameters: map[string]string{
secrets.SecretNameKey: secretName,
secrets.SecretNamespaceKey: "${pvc.namespace}",
},
}

req := &api.VolumeCreateRequest{
Locator: &api.VolumeLocator{
Name: name,
VolumeLabels: map[string]string{
secrets.SecretNameKey: secretName,
secrets.SecretTokenKey: tokenKey,
secrets.SecretNamespaceKey: namespace,
PVCNameLabelKey: pvcName,
PVCNamespaceLabelKey: namespace,
},
},
Source: &api.Source{},
Expand All @@ -2461,6 +2484,11 @@ func TestMiddlewareVolumeDeleteFailureIncorrectToken(t *testing.T) {
},
}

testVolDriver.MockK8sOps().EXPECT().
GetPersistentVolumeClaim(pvcName, namespace).Return(&pvc, nil)
testVolDriver.MockK8sOps().EXPECT().
GetStorageClassForPVC(&pvc).Return(&storageClass, nil)

mockSecret.EXPECT().
String().
Return(lsecrets.TypeK8s).
Expand Down Expand Up @@ -2525,18 +2553,38 @@ func testMiddlewareCreateVolume(
size := uint64(1234)
secretName := "secret-name"
namespace := "ns"
tokenKey := "token-key"
pvcName := "mypvc"
storageClassName := "storageclass1"
// get token
token, err := createToken("test", "system.admin", testSharedSecret)
assert.NoError(t, err)

pvc := corev1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: pvcName,
Namespace: namespace,
},
Spec: corev1.PersistentVolumeClaimSpec{
StorageClassName: &storageClassName,
},
}

storageClass := storagev1.StorageClass{
ObjectMeta: metav1.ObjectMeta{
Name: storageClassName,
},
Parameters: map[string]string{
secrets.SecretNameKey: secretName,
secrets.SecretNamespaceKey: "${pvc.namespace}",
},
}

req := &api.VolumeCreateRequest{
Locator: &api.VolumeLocator{
Name: name,
VolumeLabels: map[string]string{
secrets.SecretNameKey: secretName,
secrets.SecretTokenKey: tokenKey,
secrets.SecretNamespaceKey: namespace,
PVCNameLabelKey: pvcName,
PVCNamespaceLabelKey: namespace,
},
},
Source: &api.Source{},
Expand All @@ -2547,6 +2595,11 @@ func testMiddlewareCreateVolume(
Shared: true,
},
}
testVolDriver.MockK8sOps().EXPECT().
GetPersistentVolumeClaim(pvcName, namespace).Return(&pvc, nil)
testVolDriver.MockK8sOps().EXPECT().
GetStorageClassForPVC(&pvc).Return(&storageClass, nil)

mockSecret.EXPECT().
String().
Return(lsecrets.TypeK8s).
Expand Down
29 changes: 23 additions & 6 deletions cmd/osd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ import (
"github.com/codegangsta/cli"
"github.com/docker/docker/pkg/reexec"
"github.com/libopenstorage/openstorage/api"
"github.com/libopenstorage/openstorage/api/flexvolume"
"github.com/libopenstorage/openstorage/api/server"
"github.com/libopenstorage/openstorage/api/server/sdk"
osdcli "github.com/libopenstorage/openstorage/cli"
Expand All @@ -53,6 +52,7 @@ import (
"github.com/libopenstorage/openstorage/schedpolicy"
"github.com/libopenstorage/openstorage/volume"
volumedrivers "github.com/libopenstorage/openstorage/volume/drivers"
"github.com/libopenstorage/secrets"
"github.com/portworx/kvdb"
"github.com/portworx/kvdb/consul"
etcd "github.com/portworx/kvdb/etcd/v2"
Expand Down Expand Up @@ -183,6 +183,11 @@ func main() {
Usage: "CSI Driver name",
Value: "",
},
cli.StringFlag{
Name: "secrets-type",
Usage: "Secrets manager type. For example \"k8s\"",
Value: "",
},
}
app.Action = wrapAction(start)
app.Commands = []cli.Command{
Expand Down Expand Up @@ -334,6 +339,15 @@ func start(c *cli.Context) error {
return fmt.Errorf("Failed to initialize KVDB: %v", err)
}

// Setup secrets type if any
if secretsType := c.String("secrets-type"); len(secretsType) > 0 {
i, err := secrets.New(secretsType, nil)
if err != nil {
return fmt.Errorf("Failed to set secrets type: %v", err)
}
secrets.SetInstance(i)
}

// Get authenticators
authenticators := make(map[string]auth.Authenticator)
selfSigned, err := selfSignedAuth(c)
Expand Down Expand Up @@ -393,6 +407,9 @@ func start(c *cli.Context) error {
isDefaultSet := false
// Start the volume drivers.
for d, v := range cfg.Osd.Drivers {
// Override sched driver with the current one
server.OverrideSchedDriverName = d

logrus.Infof("Starting volume driver: %v", d)
if err := volumedrivers.Register(d, v); err != nil {
return fmt.Errorf("Unable to start volume driver: %v, %v", d, err)
Expand Down Expand Up @@ -432,11 +449,15 @@ func start(c *cli.Context) error {
return fmt.Errorf("Unable to start plugin api server: %v", err)
}

authEnabled := len(authenticators) > 0
if authEnabled {
logrus.Info("Management API (deprecated) starting with authentication enabled")
}
if _, _, err := server.StartVolumeMgmtAPI(
d, sdksocket,
volume.DriverAPIBase,
uint16(mgmtPort),
false,
authEnabled,
authenticators,
); err != nil {
return fmt.Errorf("Unable to start volume mgmt api server: %v", err)
Expand Down Expand Up @@ -512,10 +533,6 @@ func start(c *cli.Context) error {
return fmt.Errorf("Invalid OSD config file: Default Driver specified but driver not initialized")
}

if err := flexvolume.StartFlexVolumeAPI(config.FlexVolumePort, cfg.Osd.ClusterConfig.DefaultDriver); err != nil {
return fmt.Errorf("Unable to start flexvolume API: %v", err)
}

// Start the graph drivers.
for d := range cfg.Osd.GraphDrivers {
logrus.Infof("Starting graph driver: %v", d)
Expand Down
6 changes: 6 additions & 0 deletions pkg/auth/secrets/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,17 @@ const (
// SecretNameKey is a label on the openstorage.Volume object
// which corresponds to the name of the secret which holds the
// token information. Used for all secret providers
// This key supports the CSI compatible value of ${pvc.annotations['team.example.com/key']}
// as described in https://kubernetes-csi.github.io/docs/secrets-and-credentials-storage-class.html
// to specify to use the annotations on the pvc.
SecretNameKey = "openstorage.io/auth-secret-name"

// SecretNamespaceKey is a label on the openstorage.Volume object
// which corresponds to the namespace of the secret which holds the
// token information. Used for all secret providers
// This key supports the CSI compatible value of ${pvc.namespace}
// as described in https://kubernetes-csi.github.io/docs/secrets-and-credentials-storage-class.html
// to specify to use the namespace of the pvc
SecretNamespaceKey = "openstorage.io/auth-secret-namespace"

// SecretTokenKey corresponds to the key at which the auth token is stored
Expand Down
39 changes: 39 additions & 0 deletions pkg/util/template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
Copyright 2017 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 util

import (
"fmt"
"os"

"k8s.io/apimachinery/pkg/util/sets"
)

func ResolveTemplate(template string, params map[string]string) (string, error) {
missingParams := sets.NewString()
resolved := os.Expand(template, func(k string) string {
v, ok := params[k]
if !ok {
missingParams.Insert(k)
}
return v
})
if missingParams.Len() > 0 {
return "", fmt.Errorf("invalid tokens: %q", missingParams.List())
}
return resolved, nil
}
8 changes: 8 additions & 0 deletions test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@


# To update modules

```
npm install --save-dev bats bats-assert bats-support
```

115 changes: 115 additions & 0 deletions test/lib/osd.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@

TMPDIR="${BATS_TMPDIR:-/tmp}"
KIND_CLUSTER="${KIND_CLUSTER:-lpabon-kind-csi}"


# Only show output of the program on failure
function osd::suppress() {
(
local output=/tmp/output.$$
rm --force ${output} 2> /dev/null
${1+"$@"} > ${output} 2>&1
result=$?
if [ $result -ne 0 ] ; then
cat ${output}
fi
rm ${output}
exit $result
)
}

# TAP message
function osd::echo() {
if [ $DEBUG -eq 1 ] ; then
echo "# ${1}" >&3
fi
}

# TAP compliant steps which can be printed out
function osd::by() {
if [ $DEBUG -eq 1 ] ; then
echo "# STEP: ${1}" >&3
fi
}

# Get the Kind cluster IP from docker
function osd::clusterip() {
docker inspect ${CLUSTER_CONTROL_PLANE_CONTAINER} | jq -r '.[].NetworkSettings.Networks.bridge.IPAddress'
}

# Return the SDK REST Gateway address
function osd::getSdkRestGWEndpoint() {
local clusterip=$(osd::clusterip)
local nodeport=$(kubectl -n kube-system get svc portworx-api -o json | jq '.spec.ports[2].nodePort')
echo ${clusterip}:${nodeport}
}

# Return the SDK gRPC endpoint
function osd::getSdkEndpoint() {
local clusterip=$(osd::clusterip)
local nodeport=$(kubectl -n kube-system get svc portworx-api -o json | jq '.spec.ports[1].nodePort')
echo ${clusterip}:${nodeport}
}

# Creats a user in Kubernetes only. Use osd::createUserKubeconfig() instead to create a full
# kubeconfig for the new user.
function osd::createUser() {
local username="$1"
local location="$2"

openssl req -new -newkey rsa:4096 -nodes \
-keyout ${location}/${username}-k8s.key \
-out ${location}/${username}-k8s.csr \
-subj "/CN=${username}/O=openstorage"

cat <<EOF | kubectl apply -f -
apiVersion: certificates.k8s.io/v1beta1
kind: CertificateSigningRequest
metadata:
name: ${username}-access
spec:
request: $(cat ${location}/${username}-k8s.csr | base64 | tr -d '\n')
usages:
- client auth
EOF
kubectl certificate approve ${username}-access
kubectl get csr ${username}-access \
-o jsonpath='{.status.certificate}' | base64 --decode > ${location}/${username}-kubeconfig.crt
}

# Creates a new Kubernetes user only able to access their namespace with the
# same name. The kubeconfig for this user must be passed in.
function osd::createUserKubeconfig() {
local user="$1"
local location="$2"
local kubeconfig="${location}/${user}-kubeconfig.conf"

osd::createUser "$user" "$location"

kind export kubeconfig --kubeconfig=${kubeconfig} --name ${KIND_CLUSTER}
kubectl config set-credentials \
${user} \
--client-certificate=${location}/${user}-kubeconfig.crt \
--client-key=${location}/${user}-k8s.key \
--embed-certs \
--kubeconfig=${kubeconfig}
kubectl create namespace ${user}
kubectl --kubeconfig=${kubeconfig} config set-context ${user} \
--cluster=kind-${KIND_CLUSTER} \
--user=${user} \
--namespace=${user}
kubectl --kubeconfig=${kubeconfig} config use-context ${user}
kubectl create rolebinding ${user}-admin --namespace=${user} --clusterrole=admin --user=${user}
}

# Delete an object in Kubernetes and wait until fully removed
function osd::kubeDeleteObjectAndWait() {
local secs="$1"
local kubeargs="$2"
local object="$3"
local name="$4"

kubectl ${kubeargs} delete ${object} ${name}

timeout $secs sh -c "while kubectl ${kubeargs} get ${object} ${name} > /dev/null 2>&1; do sleep 1; done "
}
1 change: 1 addition & 0 deletions test/node_modules/.bin/bats
39 changes: 39 additions & 0 deletions test/node_modules/bats-assert/CHANGELOG.md
116 changes: 116 additions & 0 deletions test/node_modules/bats-assert/LICENSE
712 changes: 712 additions & 0 deletions test/node_modules/bats-assert/README.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions test/node_modules/bats-assert/load.bash
128 changes: 128 additions & 0 deletions test/node_modules/bats-assert/package.json
755 changes: 755 additions & 0 deletions test/node_modules/bats-assert/src/assert.bash

Large diffs are not rendered by default.

46 changes: 46 additions & 0 deletions test/node_modules/bats-support/CHANGELOG.md
116 changes: 116 additions & 0 deletions test/node_modules/bats-support/LICENSE
189 changes: 189 additions & 0 deletions test/node_modules/bats-support/README.md
3 changes: 3 additions & 0 deletions test/node_modules/bats-support/load.bash
104 changes: 104 additions & 0 deletions test/node_modules/bats-support/package.json
41 changes: 41 additions & 0 deletions test/node_modules/bats-support/src/error.bash
73 changes: 73 additions & 0 deletions test/node_modules/bats-support/src/lang.bash
279 changes: 279 additions & 0 deletions test/node_modules/bats-support/src/output.bash
53 changes: 53 additions & 0 deletions test/node_modules/bats/LICENSE.md
604 changes: 604 additions & 0 deletions test/node_modules/bats/README.md

Large diffs are not rendered by default.

50 changes: 50 additions & 0 deletions test/node_modules/bats/bin/bats
158 changes: 158 additions & 0 deletions test/node_modules/bats/libexec/bats-core/bats
63 changes: 63 additions & 0 deletions test/node_modules/bats/libexec/bats-core/bats-exec-suite
421 changes: 421 additions & 0 deletions test/node_modules/bats/libexec/bats-core/bats-exec-test

Large diffs are not rendered by default.

177 changes: 177 additions & 0 deletions test/node_modules/bats/libexec/bats-core/bats-format-tap-stream
55 changes: 55 additions & 0 deletions test/node_modules/bats/libexec/bats-core/bats-preprocess
10 changes: 10 additions & 0 deletions test/node_modules/bats/man/Makefile
5 changes: 5 additions & 0 deletions test/node_modules/bats/man/README.md
107 changes: 107 additions & 0 deletions test/node_modules/bats/man/bats.1
112 changes: 112 additions & 0 deletions test/node_modules/bats/man/bats.1.ronn
178 changes: 178 additions & 0 deletions test/node_modules/bats/man/bats.7
Loading