Skip to content

Commit 7e3e057

Browse files
authored
OWLS 84741: Scaling failed on Jenkins when setting Dedicated to true & io.kubernetes.client.openapi.ApiException: Not Found (#1990)
* Use REST client's access token for authentication and authorization * Enable testDedicatedModeSameNamespaceScale * Add patch permissions to rolebinding * Code cleanup * Changes from initial code review * Use TuningParameters to acccess property to control Operator's REST API authentiction and authorization implementation * Code review changes * documentation updates * document patch verb * documentation changes based on review * use code font for appropriate parameters
1 parent 3071e2d commit 7e3e057

File tree

11 files changed

+223
-10
lines changed

11 files changed

+223
-10
lines changed

docs-source/content/userguide/managing-domains/domain-lifecycle/scaling.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,27 @@ For example, when using `curl`:
8787
curl -v -k -H X-Requested-By:MyClient -H Content-Type:application/json -H Accept:application/json -H "Authorization:Bearer ..." -d '{ "managedServerCount": 3 }' https://.../scaling
8888
```
8989

90-
If you omit the header, you'll get a `400 (bad request)` response. If you omit the Bearer Authentication header, then you'll get a `401 (Unauthorized)` response.
90+
If you omit the header, you'll get a `400 (bad request)` response. If you omit the Bearer Authentication header, then you'll get a `401 (Unauthorized)` response. If the service account or user associated with the `Bearer` token does not have permission to `patch` the WebLogic domain resource, then you'll get a `403 (Forbidden)` response.
9191

92+
{{% notice note %}}
93+
To resolve a `403 (Forbidden)` response, when calling the operator's REST scaling API, you may need to add the `patch` request verb to the cluster role associated with the WebLogic `domains` resource.
94+
The example ClusterRole definition below grants `get`, `list`, `patch` and `update` access to the WebLogic `domains` resource
95+
{{% /notice %}}
96+
97+
```
98+
kind: ClusterRole
99+
apiVersion: rbac.authorization.k8s.io/v1beta1
100+
metadata:
101+
name: weblogic-domain-cluster-role
102+
rules:
103+
- apiGroups: ["weblogic.oracle"]
104+
resources: ["domains"]
105+
verbs: ["get", "list", "patch", update"]
106+
- apiGroups: ["apiextensions.k8s.io"]
107+
resources: ["customresourcedefinitions"]
108+
verbs: ["get", "list"]
109+
---
110+
```
92111
##### Operator REST endpoints
93112

94113
The WebLogic Server Kubernetes Operator can expose both an internal and external REST HTTPS endpoint.
@@ -207,7 +226,7 @@ metadata:
207226
rules:
208227
- apiGroups: ["weblogic.oracle"]
209228
resources: ["domains"]
210-
verbs: ["get", "list", "update"]
229+
verbs: ["get", "list", "patch", update"]
211230
- apiGroups: ["apiextensions.k8s.io"]
212231
resources: ["customresourcedefinitions"]
213232
verbs: ["get", "list"]

docs-source/content/userguide/managing-operators/using-the-operator/the-rest-api.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ You can access most of the REST services using `GET`, for example:
1414
* To obtain a list of domains, send a `GET` request to the URL `/operator/latest/domains`
1515
* To obtain a list of clusters in a domain, send a `GET` request to the URL `/operator/latest/domains/<domainUID>/clusters`
1616

17-
All of the REST services require authentication. Callers must pass in a valid token header and a CA certificate file. Callers should pass in the `Accept:/application/json` header.
17+
All of the REST services require authentication. Callers must pass in a valid token header and a CA certificate file. In previous operator versions, the operator performed authentication and authorization checks using the Kubernetes token review and subject access review APIs, and then updated the Domain resource using the operator's privileges. Now, by default, the operator will use the caller's bearer token to perform the underlying update to the Domain resource using the caller's privileges and thus delegating authentication and authorization checks directly to the Kubernetes API Server (see [REST interface configuration]({{< relref "/userguide/managing-operators/using-the-operator/using-helm.md#rest-interface-configuration" >}})).
18+
{{% notice note %}}
19+
When using the operator's REST services to scale up or down a WebLogic cluster, you may need to grant `patch` access to the user or service account associated with the caller's bearer token. This can be done with an RBAC ClusterRoleBinding between the user or service account and the ClusterRole that defines the permissions for the WebLogic `domains` resource.
20+
{{% /notice %}}
21+
22+
Callers should pass in the `Accept:/application/json` header.
1823

1924
To protect against Cross Site Request Forgery (CSRF) attacks, the operator REST API requires that you send in a `X-Requested-By` header when you invoke a REST endpoint that makes a change (for example, when you POST to the `/scale` endpoint). The value is an arbitrary name such as `MyClient`. For example, when using `curl`:
2025

docs-source/content/userguide/managing-operators/using-the-operator/using-helm.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,22 @@ Example:
468468
```
469469
externalOperatorKey: QmFnIEF0dHJpYnV0ZXMKICAgIGZyaWVuZGx5TmFtZTogd2VibG9naWMtb3B ...
470470
```
471-
471+
##### `tokenReviewAuthentication`
472+
If set to `true`, `tokenReviewAuthentication` specifies whether the the operator's REST API should use:
473+
* Kubernetes token review API for authenticating users and
474+
* Kubernetes subject access review API for authorizing a user's operation (`get`, `list`,
475+
`patch`, and such) on a resource.
476+
* Update the Domain resource using the operator's privileges.
477+
478+
If set to `false`, the operator's REST API will use the caller's bearer token for any update
479+
to the Domain resource so that it is done using the caller's privileges.
480+
481+
Defaults to `false`.
482+
483+
Example:
484+
```
485+
tokenReviewAuthentication: true
486+
```
472487
#### Debugging options
473488

474489
##### `remoteDebugNodePortEnabled`

integration-tests/src/test/java/oracle/weblogic/kubernetes/ItDedicatedMode.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,6 @@ public void testDedicatedModeSameNamespace() {
201201
* scaling up cluster-1 in domain1Namespace succeeds.
202202
*/
203203
@Test
204-
@Disabled("Disable the test due to JIRA OWLS-84741")
205204
@Order(3)
206205
@DisplayName("Scale up cluster-1 in domain1Namespace and verify it succeeds")
207206
public void testDedicatedModeSameNamespaceScale() {

integration-tests/src/test/java/oracle/weblogic/kubernetes/actions/impl/Domain.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,7 @@ private static boolean createRbacApiObjectsForWLDFScript(String domainNamespace,
566566
.addResourcesItem("domains")
567567
.addVerbsItem("get")
568568
.addVerbsItem("list")
569+
.addVerbsItem("patch")
569570
.addVerbsItem("update"))
570571
.addRulesItem(new V1PolicyRule()
571572
.addApiGroupsItem("apiextensions.k8s.io")

kubernetes/charts/weblogic-operator/templates/_operator-cm.tpl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ data:
3636
{{- if .clusterSizePaddingValidationEnabled }}
3737
clusterSizePaddingValidationEnabled: {{ .clusterSizePaddingValidationEnabled | quote }}
3838
{{- end }}
39+
{{- if .tokenReviewAuthentication }}
40+
tokenReviewAuthentication: {{ .tokenReviewAuthentication | quote }}
41+
{{- end }}
3942
kind: "ConfigMap"
4043
metadata:
4144
labels:

kubernetes/charts/weblogic-operator/values.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,3 +191,13 @@ externalServiceNameSuffix: "-ext"
191191
# cluster size.
192192
# The default value is true.
193193
clusterSizePaddingValidationEnabled: true
194+
195+
# tokenReviewAuthentication, if set to true, specifies whether the the operator's REST API should use
196+
# 1. Kubernetes token review API for authenticating users, and
197+
# 2. Kubernetes subject access review API for authorizing a user's operation (get, list,
198+
# patch, etc) on a resource.
199+
# 3. Update the Domain resource using the operator's privileges.
200+
# This parameter, if set to false, will use the caller's bearer token for any update
201+
# to the Domain resource so that it is done using the caller's privileges.
202+
# The default value is false.
203+
#tokenReviewAuthentication: false

operator/src/main/java/oracle/kubernetes/operator/helpers/CallBuilder.java

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33

44
package oracle.kubernetes.operator.helpers;
55

6+
import java.io.IOException;
67
import java.util.Optional;
78
import java.util.function.Consumer;
89
import javax.annotation.Nonnull;
910

11+
import com.google.common.base.Strings;
1012
import io.kubernetes.client.custom.V1Patch;
1113
import io.kubernetes.client.openapi.ApiCallback;
1214
import io.kubernetes.client.openapi.ApiClient;
@@ -42,6 +44,8 @@
4244
import io.kubernetes.client.openapi.models.V1TokenReview;
4345
import io.kubernetes.client.openapi.models.V1beta1CustomResourceDefinition;
4446
import io.kubernetes.client.openapi.models.VersionInfo;
47+
import io.kubernetes.client.util.ClientBuilder;
48+
import io.kubernetes.client.util.credentials.AccessTokenAuthentication;
4549
import okhttp3.Call;
4650
import oracle.kubernetes.operator.TuningParameters;
4751
import oracle.kubernetes.operator.TuningParameters.CallBuilderTuning;
@@ -87,7 +91,7 @@ public <T> T execute(
8791
private static SynchronousCallDispatcher DISPATCHER = DEFAULT_DISPATCHER;
8892
private static final AsyncRequestStepFactory DEFAULT_STEP_FACTORY = AsyncRequestStep::new;
8993
private static AsyncRequestStepFactory STEP_FACTORY = DEFAULT_STEP_FACTORY;
90-
private final ClientPool helper;
94+
private ClientPool helper;
9195
private final Boolean allowWatchBookmarks = false;
9296
private final String dryRun = null;
9397
private final String pretty = "false";
@@ -469,6 +473,10 @@ private CallBuilder(CallBuilderTuning tuning, ClientPool helper) {
469473
this.helper = helper;
470474
}
471475

476+
public CallBuilder(ClientPool pool) {
477+
this(getCallBuilderTuning(), pool);
478+
}
479+
472480
private static CallBuilderTuning getCallBuilderTuning() {
473481
return Optional.ofNullable(TuningParameters.getInstance())
474482
.map(TuningParameters::getCallBuilderTuning)
@@ -1939,4 +1947,31 @@ private <T> Step createRequestAsync(
19391947
private CancellableCall wrap(Call call) {
19401948
return new CallWrapper(call);
19411949
}
1950+
1951+
public ClientPool getClientPool() {
1952+
return this.helper;
1953+
}
1954+
1955+
/**
1956+
* Create AccessTokenAuthentication component for authenticating user represented by
1957+
* the given token.
1958+
* @param accessToken - User's Bearer token
1959+
* @return - this CallBuilder instance
1960+
*/
1961+
public CallBuilder withAuthentication(String accessToken) {
1962+
if (!Strings.isNullOrEmpty(accessToken)) {
1963+
this.helper = new ClientPool().withApiClient(createApiClient(accessToken));
1964+
}
1965+
return this;
1966+
}
1967+
1968+
private ApiClient createApiClient(String accessToken) {
1969+
try {
1970+
ClientBuilder builder = ClientBuilder.standard();
1971+
return builder.setAuthentication(
1972+
new AccessTokenAuthentication(accessToken)).build();
1973+
} catch (IOException e) {
1974+
throw new RuntimeException(e);
1975+
}
1976+
}
19421977
}

operator/src/main/java/oracle/kubernetes/operator/helpers/ClientPool.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ private ApiClient getApiClient() {
9595
return client;
9696
}
9797

98+
public ClientPool withApiClient(ApiClient apiClient) {
99+
instance.getAndSet(apiClient);
100+
return this;
101+
}
102+
98103
private static class DefaultClientFactory implements ClientFactory {
99104
private final AtomicBoolean first = new AtomicBoolean(true);
100105

operator/src/main/java/oracle/kubernetes/operator/rest/RestBackendImpl.java

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import io.kubernetes.client.openapi.models.V1TokenReviewStatus;
2727
import io.kubernetes.client.openapi.models.V1UserInfo;
2828
import oracle.kubernetes.operator.Main;
29+
import oracle.kubernetes.operator.TuningParameters;
2930
import oracle.kubernetes.operator.helpers.AuthenticationProxy;
3031
import oracle.kubernetes.operator.helpers.AuthorizationProxy;
3132
import oracle.kubernetes.operator.helpers.AuthorizationProxy.Operation;
@@ -55,7 +56,7 @@ public class RestBackendImpl implements RestBackend {
5556
private static final LoggingFacade LOGGER = LoggingFactory.getLogger("Operator", "Operator");
5657
private static final String NEW_CLUSTER_REPLICAS =
5758
"{'clusterName':'%s','replicas':%d}".replaceAll("'", "\"");
58-
public static final String INITIAL_VERSION = "1";
59+
private static final String INITIAL_VERSION = "1";
5960

6061
@SuppressWarnings("FieldMayBeFinal") // used by unit test
6162
private static TopologyRetriever INSTANCE =
@@ -68,10 +69,11 @@ public class RestBackendImpl implements RestBackend {
6869
};
6970

7071
private final AuthenticationProxy atn = new AuthenticationProxy();
71-
private final AuthorizationProxy atz = new AuthorizationProxy();
72+
private AuthorizationProxy atz = new AuthorizationProxy();
7273
private final String principal;
7374
private final Supplier<Collection<String>> domainNamespaces;
7475
private V1UserInfo userInfo;
76+
private CallBuilder callBuilder;
7577

7678
/**
7779
* Construct a RestBackendImpl that is used to handle one WebLogic operator REST request.
@@ -85,10 +87,16 @@ public class RestBackendImpl implements RestBackend {
8587
this.domainNamespaces = domainNamespaces;
8688
this.principal = principal;
8789
userInfo = authenticate(accessToken);
90+
callBuilder = userInfo != null ? new CallBuilder() :
91+
new CallBuilder().withAuthentication(accessToken);
92+
LOGGER.exiting();
8893
}
8994

9095
private void authorize(String domainUid, Operation operation) {
9196
LOGGER.entering(domainUid, operation);
97+
if (!authenticateWithTokenReview()) {
98+
return;
99+
}
92100
boolean authorized;
93101
if (domainUid == null) {
94102
authorized =
@@ -128,6 +136,9 @@ private String getNamespace(String domainUid) {
128136

129137
private V1UserInfo authenticate(String accessToken) {
130138
LOGGER.entering();
139+
if (!authenticateWithTokenReview()) {
140+
return null;
141+
}
131142
V1TokenReviewStatus status = atn.check(principal, accessToken,
132143
Main.isDedicated() ? getOperatorNamespace() : null);
133144
if (status == null) {
@@ -170,7 +181,7 @@ private Stream<Domain> getDomainStream() {
170181

171182
private List<Domain> getDomains(String ns) {
172183
try {
173-
return new CallBuilder().listDomain(ns).getItems();
184+
return callBuilder.listDomain(ns).getItems();
174185
} catch (ApiException e) {
175186
throw handleApiException(e);
176187
}
@@ -315,7 +326,7 @@ private void patchClusterReplicas(Domain domain, String cluster, int replicas) {
315326

316327
private void patchDomain(Domain domain, JsonPatchBuilder patchBuilder) {
317328
try {
318-
new CallBuilder()
329+
callBuilder
319330
.patchDomain(
320331
domain.getDomainUid(), domain.getMetadata().getNamespace(),
321332
new V1Patch(patchBuilder.build().toString()));
@@ -401,6 +412,25 @@ private WebApplicationException createWebApplicationException(int status, String
401412
return new WebApplicationException(rb.build());
402413
}
403414

415+
protected boolean authenticateWithTokenReview() {
416+
return "true".equalsIgnoreCase(Optional.ofNullable(TuningParameters.getInstance().get("tokenReviewAuthentication"))
417+
.orElse("false"));
418+
}
419+
420+
V1UserInfo getUserInfo() {
421+
return userInfo;
422+
}
423+
424+
// Intended for unit tests
425+
RestBackendImpl withAuthorizationProxy(AuthorizationProxy authorizationProxy) {
426+
this.atz = authorizationProxy;
427+
return this;
428+
}
429+
430+
CallBuilder getCallBuilder() {
431+
return this.callBuilder;
432+
}
433+
404434
interface TopologyRetriever {
405435
WlsDomainConfig getWlsDomainConfig(String ns, String domainUid);
406436
}

0 commit comments

Comments
 (0)