github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/worker/caasadmission/handler_test.go (about)

     1  // Copyright 2020 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package caasadmission
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/json"
     9  	"errors"
    10  	"fmt"
    11  	"net/http"
    12  	"net/http/httptest"
    13  	"strings"
    14  
    15  	"github.com/juju/loggo"
    16  	jc "github.com/juju/testing/checkers"
    17  	gc "gopkg.in/check.v1"
    18  	admission "k8s.io/api/admission/v1beta1"
    19  	authentication "k8s.io/api/authentication/v1"
    20  	core "k8s.io/api/core/v1"
    21  	meta "k8s.io/apimachinery/pkg/apis/meta/v1"
    22  	"k8s.io/apimachinery/pkg/runtime"
    23  	"k8s.io/apimachinery/pkg/runtime/schema"
    24  	"k8s.io/apimachinery/pkg/types"
    25  
    26  	providerconst "github.com/juju/juju/caas/kubernetes/provider/constants"
    27  	providerutils "github.com/juju/juju/caas/kubernetes/provider/utils"
    28  	rbacmappertest "github.com/juju/juju/worker/caasrbacmapper/test"
    29  )
    30  
    31  type HandlerSuite struct {
    32  	logger Logger
    33  }
    34  
    35  var _ = gc.Suite(&HandlerSuite{})
    36  
    37  func (h *HandlerSuite) SetUpTest(c *gc.C) {
    38  	h.logger = loggo.Logger{}
    39  }
    40  
    41  func (h *HandlerSuite) TestCompareGroupVersionKind(c *gc.C) {
    42  	tests := []struct {
    43  		A           *schema.GroupVersionKind
    44  		B           *schema.GroupVersionKind
    45  		ShouldMatch bool
    46  	}{
    47  		{
    48  			A: &schema.GroupVersionKind{
    49  				Group:   admission.SchemeGroupVersion.Group,
    50  				Version: admission.SchemeGroupVersion.Version,
    51  				Kind:    "AdmissionReview",
    52  			},
    53  			B: &schema.GroupVersionKind{
    54  				Group:   admission.SchemeGroupVersion.Group,
    55  				Version: admission.SchemeGroupVersion.Version,
    56  				Kind:    "AdmissionReview",
    57  			},
    58  			ShouldMatch: true,
    59  		},
    60  		{
    61  			A: &schema.GroupVersionKind{
    62  				Group:   admission.SchemeGroupVersion.Group,
    63  				Version: admission.SchemeGroupVersion.Version,
    64  				Kind:    "AdmissionReview",
    65  			},
    66  			B: &schema.GroupVersionKind{
    67  				Group:   admission.SchemeGroupVersion.Group,
    68  				Version: admission.SchemeGroupVersion.Version,
    69  				Kind:    "Junk",
    70  			},
    71  			ShouldMatch: false,
    72  		},
    73  		{
    74  			A: &schema.GroupVersionKind{
    75  				Group:   admission.SchemeGroupVersion.Group,
    76  				Version: admission.SchemeGroupVersion.Version,
    77  				Kind:    "AdmissionReview",
    78  			},
    79  			B:           nil,
    80  			ShouldMatch: false,
    81  		},
    82  	}
    83  
    84  	for _, test := range tests {
    85  		c.Assert(compareGroupVersionKind(test.A, test.B), gc.Equals, test.ShouldMatch)
    86  	}
    87  }
    88  
    89  func (h *HandlerSuite) TestEmptyBodyFails(c *gc.C) {
    90  	req := httptest.NewRequest(http.MethodPost, "/", nil)
    91  	recorder := httptest.NewRecorder()
    92  
    93  	admissionHandler(h.logger, &rbacmappertest.Mapper{}, false).ServeHTTP(recorder, req)
    94  
    95  	c.Assert(recorder.Code, gc.Equals, http.StatusBadRequest)
    96  }
    97  
    98  func (h *HandlerSuite) TestUnknownContentType(c *gc.C) {
    99  	req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("junk"))
   100  	req.Header.Set("junk", "junk")
   101  	recorder := httptest.NewRecorder()
   102  
   103  	admissionHandler(h.logger, &rbacmappertest.Mapper{}, false).ServeHTTP(recorder, req)
   104  
   105  	c.Assert(recorder.Code, gc.Equals, http.StatusUnsupportedMediaType)
   106  }
   107  
   108  func (h *HandlerSuite) TestUnknownServiceAccount(c *gc.C) {
   109  	inReview := &admission.AdmissionReview{
   110  		Request: &admission.AdmissionRequest{
   111  			UID: types.UID("test"),
   112  			UserInfo: authentication.UserInfo{
   113  				UID: "juju-tst-sa",
   114  			},
   115  		},
   116  	}
   117  
   118  	body, err := json.Marshal(inReview)
   119  	c.Assert(err, gc.IsNil)
   120  
   121  	req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body))
   122  	req.Header.Set(HeaderContentType, ExpectedContentType)
   123  	recorder := httptest.NewRecorder()
   124  
   125  	admissionHandler(h.logger, &rbacmappertest.Mapper{}, false).ServeHTTP(recorder, req)
   126  	c.Assert(recorder.Code, gc.Equals, http.StatusOK)
   127  	c.Assert(recorder.Body, gc.NotNil)
   128  
   129  	outReview := admission.AdmissionReview{}
   130  	err = json.Unmarshal(recorder.Body.Bytes(), &outReview)
   131  	c.Assert(err, jc.ErrorIsNil)
   132  
   133  	c.Assert(outReview.Response.Allowed, jc.IsTrue)
   134  	c.Assert(outReview.Response.UID, gc.Equals, inReview.Request.UID)
   135  }
   136  
   137  func (h *HandlerSuite) TestRBACMapperFailure(c *gc.C) {
   138  	inReview := &admission.AdmissionReview{
   139  		Request: &admission.AdmissionRequest{
   140  			UID: types.UID("test"),
   141  			UserInfo: authentication.UserInfo{
   142  				UID: "juju-tst-sa",
   143  			},
   144  		},
   145  	}
   146  
   147  	body, err := json.Marshal(inReview)
   148  	c.Assert(err, gc.IsNil)
   149  
   150  	req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body))
   151  	req.Header.Set(HeaderContentType, ExpectedContentType)
   152  	recorder := httptest.NewRecorder()
   153  
   154  	rbacMapper := rbacmappertest.Mapper{
   155  		AppNameForServiceAccountFunc: func(_ types.UID) (string, error) {
   156  			return "", errors.New("test error")
   157  		},
   158  	}
   159  
   160  	admissionHandler(h.logger, &rbacMapper, false).ServeHTTP(recorder, req)
   161  	c.Assert(recorder.Code, gc.Equals, http.StatusInternalServerError)
   162  }
   163  
   164  func (h *HandlerSuite) TestPatchLabelsAdd(c *gc.C) {
   165  	pod := core.Pod{
   166  		ObjectMeta: meta.ObjectMeta{
   167  			Name: "pod",
   168  		},
   169  	}
   170  	podBytes, err := json.Marshal(&pod)
   171  	c.Assert(err, jc.ErrorIsNil)
   172  
   173  	inReview := &admission.AdmissionReview{
   174  		Request: &admission.AdmissionRequest{
   175  			UID: types.UID("test"),
   176  			UserInfo: authentication.UserInfo{
   177  				UID: "juju-tst-sa",
   178  			},
   179  			Object: runtime.RawExtension{
   180  				Raw: podBytes,
   181  			},
   182  		},
   183  	}
   184  
   185  	body, err := json.Marshal(inReview)
   186  	c.Assert(err, jc.ErrorIsNil)
   187  
   188  	req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body))
   189  	req.Header.Set(HeaderContentType, ExpectedContentType)
   190  	recorder := httptest.NewRecorder()
   191  
   192  	appName := "test-app"
   193  	rbacMapper := rbacmappertest.Mapper{
   194  		AppNameForServiceAccountFunc: func(_ types.UID) (string, error) {
   195  			return appName, nil
   196  		},
   197  	}
   198  
   199  	admissionHandler(h.logger, &rbacMapper, false).ServeHTTP(recorder, req)
   200  	c.Assert(recorder.Code, gc.Equals, http.StatusOK)
   201  	c.Assert(recorder.Body, gc.NotNil)
   202  
   203  	outReview := admission.AdmissionReview{}
   204  	err = json.Unmarshal(recorder.Body.Bytes(), &outReview)
   205  	c.Assert(err, jc.ErrorIsNil)
   206  
   207  	c.Assert(outReview.Response.Allowed, jc.IsTrue)
   208  	c.Assert(outReview.Response.UID, gc.Equals, inReview.Request.UID)
   209  
   210  	patchOperations := []patchOperation{}
   211  	err = json.Unmarshal(outReview.Response.Patch, &patchOperations)
   212  	c.Assert(err, jc.ErrorIsNil)
   213  
   214  	c.Assert(len(patchOperations), gc.Equals, 2)
   215  	c.Assert(patchOperations[0].Op, gc.Equals, "add")
   216  	c.Assert(patchOperations[0].Path, gc.Equals, "/metadata/labels")
   217  
   218  	expectedLabels := providerutils.LabelForKeyValue(
   219  		providerconst.LabelJujuAppCreatedBy, appName)
   220  
   221  	for k, v := range expectedLabels {
   222  		found := false
   223  		for _, patchOp := range patchOperations[1:] {
   224  			if patchOp.Path == fmt.Sprintf("/metadata/labels/%s", patchEscape(k)) {
   225  				c.Assert(patchOp.Op, gc.Equals, "add")
   226  				c.Assert(patchOp.Value, jc.DeepEquals, v)
   227  				found = true
   228  				break
   229  			}
   230  		}
   231  		c.Assert(found, jc.IsTrue)
   232  	}
   233  
   234  	for k, v := range expectedLabels {
   235  		found := false
   236  		for _, op := range patchOperations {
   237  			c.Assert(op.Op, gc.Equals, addOp)
   238  			if op.Path == fmt.Sprintf("/metadata/labels/%s", patchEscape(k)) &&
   239  				op.Value.(string) == v {
   240  				found = true
   241  				break
   242  			}
   243  			continue
   244  		}
   245  		c.Assert(found, jc.IsTrue)
   246  	}
   247  }
   248  
   249  func (h *HandlerSuite) TestPatchLabelsReplace(c *gc.C) {
   250  	pod := core.Pod{
   251  		ObjectMeta: meta.ObjectMeta{
   252  			Name: "pod",
   253  			Labels: providerutils.LabelForKeyValue(
   254  				providerconst.LabelJujuAppCreatedBy, "replace-app",
   255  			),
   256  		},
   257  	}
   258  	podBytes, err := json.Marshal(&pod)
   259  	c.Assert(err, jc.ErrorIsNil)
   260  
   261  	inReview := &admission.AdmissionReview{
   262  		Request: &admission.AdmissionRequest{
   263  			UID: types.UID("test"),
   264  			UserInfo: authentication.UserInfo{
   265  				UID: "juju-tst-sa",
   266  			},
   267  			Object: runtime.RawExtension{
   268  				Raw: podBytes,
   269  			},
   270  		},
   271  	}
   272  
   273  	body, err := json.Marshal(inReview)
   274  	c.Assert(err, jc.ErrorIsNil)
   275  
   276  	req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body))
   277  	req.Header.Set(HeaderContentType, ExpectedContentType)
   278  	recorder := httptest.NewRecorder()
   279  
   280  	appName := "test-app"
   281  	rbacMapper := rbacmappertest.Mapper{
   282  		AppNameForServiceAccountFunc: func(_ types.UID) (string, error) {
   283  			return appName, nil
   284  		},
   285  	}
   286  
   287  	admissionHandler(h.logger, &rbacMapper, false).ServeHTTP(recorder, req)
   288  	c.Assert(recorder.Code, gc.Equals, http.StatusOK)
   289  	c.Assert(recorder.Body, gc.NotNil)
   290  
   291  	outReview := admission.AdmissionReview{}
   292  	err = json.Unmarshal(recorder.Body.Bytes(), &outReview)
   293  	c.Assert(err, jc.ErrorIsNil)
   294  
   295  	c.Assert(outReview.Response.Allowed, jc.IsTrue)
   296  	c.Assert(outReview.Response.UID, gc.Equals, inReview.Request.UID)
   297  
   298  	patchOperations := []patchOperation{}
   299  	err = json.Unmarshal(outReview.Response.Patch, &patchOperations)
   300  	c.Assert(err, jc.ErrorIsNil)
   301  	c.Assert(len(patchOperations), gc.Equals, 1)
   302  
   303  	expectedLabels := providerutils.LabelForKeyValue(
   304  		providerconst.LabelJujuAppCreatedBy, appName)
   305  	for k, v := range expectedLabels {
   306  		found := false
   307  		for _, patchOp := range patchOperations {
   308  			if patchOp.Path == fmt.Sprintf("/metadata/labels/%s", patchEscape(k)) {
   309  				c.Assert(patchOp.Op, gc.Equals, "replace")
   310  				c.Assert(patchOp.Value, jc.DeepEquals, v)
   311  				found = true
   312  				break
   313  			}
   314  		}
   315  		c.Assert(found, jc.IsTrue)
   316  	}
   317  
   318  	for k, v := range expectedLabels {
   319  		found := false
   320  		for _, op := range patchOperations {
   321  			c.Assert(op.Op, gc.Equals, replaceOp)
   322  			if op.Path == fmt.Sprintf("/metadata/labels/%s", patchEscape(k)) &&
   323  				op.Value.(string) == v {
   324  				found = true
   325  				break
   326  			}
   327  			continue
   328  		}
   329  		c.Assert(found, jc.IsTrue)
   330  	}
   331  }
   332  
   333  func (h *HandlerSuite) TestSelfSubjectAccessReviewIgnore(c *gc.C) {
   334  	inReview := &admission.AdmissionReview{
   335  		Request: &admission.AdmissionRequest{
   336  			Kind: meta.GroupVersionKind{
   337  				Group:   "authorization.k8s.io",
   338  				Kind:    "SelfSubjectAccessReview",
   339  				Version: "v1",
   340  			},
   341  			UID: types.UID("test"),
   342  			UserInfo: authentication.UserInfo{
   343  				UID: "juju-tst-sa",
   344  			},
   345  		},
   346  	}
   347  
   348  	body, err := json.Marshal(inReview)
   349  	c.Assert(err, jc.ErrorIsNil)
   350  
   351  	req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body))
   352  	req.Header.Set(HeaderContentType, ExpectedContentType)
   353  	recorder := httptest.NewRecorder()
   354  
   355  	appName := "test-app"
   356  	rbacMapper := rbacmappertest.Mapper{
   357  		AppNameForServiceAccountFunc: func(_ types.UID) (string, error) {
   358  			return appName, nil
   359  		},
   360  	}
   361  
   362  	admissionHandler(h.logger, &rbacMapper, false).ServeHTTP(recorder, req)
   363  	c.Assert(recorder.Code, gc.Equals, http.StatusOK)
   364  	c.Assert(recorder.Body, gc.NotNil)
   365  
   366  	outReview := admission.AdmissionReview{}
   367  	err = json.Unmarshal(recorder.Body.Bytes(), &outReview)
   368  	c.Assert(err, jc.ErrorIsNil)
   369  
   370  	c.Assert(outReview.Response.Allowed, jc.IsTrue)
   371  	c.Assert(outReview.Response.UID, gc.Equals, inReview.Request.UID)
   372  
   373  	c.Assert(len(outReview.Response.Patch), gc.Equals, 0)
   374  }