/******************************************************************************* * Copyright (c) 2015 Red Hat, Inc. Distributed under license by Red Hat, Inc. * All rights reserved. This program is made available under the terms of the * Eclipse Public License v1.0 which accompanies this distribution, and is * available at http://www.eclipse.org/legal/epl-v10.html * * Contributors: Red Hat, Inc. ******************************************************************************/ package com.openshift.internal.restclient; import static java.util.stream.Collectors.joining; import static com.openshift.internal.restclient.capability.CapabilityInitializer.initializeClientCapabilities; import java.io.IOException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.lang.ObjectUtils; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.openshift.internal.restclient.authorization.AuthorizationContext; import com.openshift.internal.restclient.okhttp.WatchClient; import com.openshift.restclient.IApiTypeMapper; import com.openshift.restclient.IClient; import com.openshift.restclient.IOpenShiftWatchListener; import com.openshift.restclient.IResourceFactory; import com.openshift.restclient.IWatcher; import com.openshift.restclient.OpenShiftException; import com.openshift.restclient.ResourceKind; import com.openshift.restclient.UnsupportedOperationException; import com.openshift.restclient.api.ITypeFactory; import com.openshift.restclient.authorization.IAuthorizationContext; import com.openshift.restclient.capability.CapabilityVisitor; import com.openshift.restclient.capability.ICapability; import com.openshift.restclient.http.IHttpConstants; import com.openshift.restclient.model.IList; import com.openshift.restclient.model.IResource; import com.openshift.restclient.model.JSONSerializeable; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; /** * @author Jeff Cantrill */ public class DefaultClient implements IClient, IHttpConstants{ public static final String SYSTEM_PROP_K8E_API_VERSION = "osjc.k8e.apiversion"; public static final String SYSTEM_PROP_OPENSHIFT_API_VERSION = "osjc.openshift.apiversion"; private static final Logger LOGGER = LoggerFactory.getLogger(DefaultClient.class); private URL baseUrl; private OkHttpClient client; private IResourceFactory factory; private Map<Class<? extends ICapability>, ICapability> capabilities = new HashMap<Class<? extends ICapability>, ICapability>(); private boolean capabilitiesInitialized = false; private static final String OS_API_ENDPOINT = "oapi"; private String openShiftVersion; private String kubernetesVersion; private AuthorizationContext authContext; private IApiTypeMapper typeMapper; public DefaultClient(URL baseUrl, OkHttpClient client, IResourceFactory factory, IApiTypeMapper typeMapper, AuthorizationContext authContext){ this.baseUrl = baseUrl; this.client = client; this.factory = factory; if(this.factory != null) { this.factory.setClient(this); } openShiftVersion = System.getProperty(SYSTEM_PROP_OPENSHIFT_API_VERSION, null); kubernetesVersion = System.getProperty(SYSTEM_PROP_K8E_API_VERSION, null); this.typeMapper = typeMapper != null ? typeMapper : new ApiTypeMapper(baseUrl.toString(), client); this.authContext = authContext; } @Override public IClient clone() { AuthorizationContext context = authContext.clone(); DefaultClient clone = new DefaultClient(baseUrl, client, factory, typeMapper, context); context.setClient(clone); return clone; } @Override public IResourceFactory getResourceFactory() { return factory; }; @Override public IWatcher watch(String namespace, IOpenShiftWatchListener listener, String...kinds) { WatchClient watcher = new WatchClient(this, this.typeMapper, this.client); return watcher.watch(Arrays.asList(kinds), namespace, listener); } @Override public IWatcher watch(IOpenShiftWatchListener listener, String...kinds) { return this.watch("", listener, kinds); } @Override public String getResourceURI(IResource resource) { return new URLBuilder(getBaseURL(), typeMapper, resource).build().toString(); } @Override public <T extends IResource> List<T> list(String kind) { return list(kind,""); } @Override public <T extends IResource> List<T> list(String kind, Map<String, String> labels) { return list(kind,"", labels); } @Override public <T extends IResource> List<T> list(String kind, String namespace) { return list(kind, namespace, new HashMap<>()); } @SuppressWarnings("unchecked") @Override public <T extends IResource> List<T> list(String kind, String namespace, Map<String, String> labels) { String labelQuery=""; if(labels != null && !labels.isEmpty()){ labelQuery = labels.entrySet().stream() .map(e -> e.getKey() + "=" + e.getValue()) .collect(joining(",")); } return list(kind, namespace, labelQuery); } @SuppressWarnings("unchecked") @Override public <T extends IResource> List<T> list(String kind, String namespace, String labelQuery) { Map<String, String> params = new HashMap<>(); if(labelQuery != null && !labelQuery.isEmpty()){ params.put("labelSelector", labelQuery); } IList resources = execute(HttpMethod.GET.toString(), kind, namespace, null, null, null, params); List<T> items = new ArrayList<>(); items.addAll((Collection<? extends T>) resources.getItems()); return items; } @Override public Collection<IResource> create(IList list, String namespace){ List<IResource> results = new ArrayList<IResource>(list.getItems().size()); for (IResource resource : list.getItems()) { try{ results.add(create(resource, namespace)); }catch(OpenShiftException e){ if(e.getStatus() != null){ results.add(e.getStatus()); }else{ throw e; } } } return results; } @Override public <T extends IResource> T create(T resource) { return create(resource, resource.getNamespace()); } @Override public <T extends IResource> T create(T resource, String namespace) { return execute(HttpMethod.POST, resource.getKind(), namespace, null, null, resource); } @Override public <T extends IResource> T create(String kind, String namespace, String name, String subresource, IResource payload) { return execute(HttpMethod.POST, kind, namespace, name, subresource, payload); } enum HttpMethod{ GET, PUT, POST, DELETE } private <T extends IResource> T execute(HttpMethod method, String kind, String namespace, String name, String subresource, IResource payload) { return execute(method.toString(), kind, namespace, name, subresource, payload); } @SuppressWarnings("unchecked") public <T extends IResource> T execute(String method, String kind, String namespace, String name, String subresource, IResource payload, String subContext) { return (T) execute(this.factory, method, kind, namespace, name, subresource, subContext, payload, Collections.emptyMap()); } @Override @SuppressWarnings("unchecked") public <T extends IResource> T execute(String method, String kind, String namespace, String name, String subresource, IResource payload) { return (T) execute(this.factory, method, kind, namespace, name, subresource, null, payload, Collections.emptyMap()); } @Override @SuppressWarnings("unchecked") public <T extends IResource> T execute(String method, String kind, String namespace, String name, String subresource, IResource payload, Map<String, String> params) { return (T) execute(this.factory, method, kind, namespace, name, subresource, null, payload,params); } @SuppressWarnings("unchecked") public <T> T execute(ITypeFactory factory, String method, String kind, String namespace, String name, String subresource, String subContext, JSONSerializeable payload, Map<String, String> params) { if(factory == null) { throw new OpenShiftException("ITypeFactory is null while trying to call IClient#execute"); } if(params == null){ params = Collections.emptyMap(); } if(ResourceKind.LIST.equals(kind)) throw new UnsupportedOperationException("Generic create operation not supported for resource type 'List'"); final URL endpoint = new URLBuilder(this.baseUrl, typeMapper) .kind(kind) .name(name) .namespace(namespace) .subresource(subresource) .subContext(subContext) .addParameters(params) .build(); try { Request request = newRequestBuilderTo(endpoint.toString()) .method(method, getPayload(method, payload)) .build(); LOGGER.debug("About to make {} request: {}", request.method(), request); try(Response result = client.newCall(request).execute()){ String response = result.body().string(); LOGGER.debug("Response: {}", response); return (T) factory.createInstanceFrom(response); } } catch (IOException e){ throw new OpenShiftException(e, "Unable to execute request to %s", endpoint); } } private RequestBody getPayload(String method, JSONSerializeable payload) { switch(method.toUpperCase()){ case "GET": case "DELETE": return null; default: String json = payload == null ? "" : payload.toJson(true); LOGGER.debug("About to send payload: {}", json); return RequestBody.create(MediaType.parse(MEDIATYPE_APPLICATION_JSON), json); } } @Override public String getServerReadyStatus() { try { Request request = new Request.Builder() .url(new URL(this.baseUrl, "healthz/ready")) .header(PROPERTY_ACCEPT, "*/*") .build(); try(Response response = client.newCall(request).execute()){ return response.body().string(); } } catch (IOException e) { throw new OpenShiftException(e, "Exception while trying to determine the health/ready response of the server"); } } public Request.Builder newRequestBuilderTo(String endpoint){ return newRequestBuilderTo(endpoint, MEDIATYPE_APPLICATION_JSON); } public Request.Builder newRequestBuilderTo(String endpoint,String acceptMediaType){ Request.Builder builder = new Request.Builder() .url(endpoint.toString()) .header(PROPERTY_ACCEPT, acceptMediaType); String token = null; if(this.authContext != null && StringUtils.isNotBlank(this.authContext.getToken())){ token = this.authContext.getToken(); } builder.header(IHttpConstants.PROPERTY_AUTHORIZATION, String.format("%s %s", IHttpConstants.AUTHORIZATION_BEARER, token)); return builder; } @Override public <T extends IResource> T update(T resource) { return execute(HttpMethod.PUT, resource.getKind(), resource.getNamespace(), resource.getName(), null, resource); } @Override public <T extends IResource> void delete(T resource) { execute(HttpMethod.DELETE, resource.getKind(), resource.getNamespace(), resource.getName(), null, resource); } @Override public IList get(String kind, String namespace) { return execute(HttpMethod.GET, kind, namespace, null, null, null); } @Override public <T extends IResource> T get(String kind, String name, String namespace) { return execute(HttpMethod.GET, kind, namespace, name, null, null); } public synchronized void initializeCapabilities(){ if(capabilitiesInitialized) return; initializeClientCapabilities(capabilities, this); capabilitiesInitialized = true; } @SuppressWarnings("unchecked") @Override public <T extends ICapability> T getCapability(Class<T> capability) { return (T) capabilities.get(capability); } @Override public boolean supports(Class<? extends ICapability> capability) { if(!capabilitiesInitialized ){ initializeCapabilities(); } return capabilities.containsKey(capability); } @SuppressWarnings("unchecked") @Override public <T extends ICapability, R> R accept(CapabilityVisitor<T, R> visitor, R unsupportedCapabililityValue){ if(!capabilitiesInitialized) initializeCapabilities(); if(capabilities.containsKey(visitor.getCapabilityType())){ T capability = (T) capabilities.get(visitor.getCapabilityType()); return (R) visitor.visit(capability); } return unsupportedCapabililityValue; } @Override public String getOpenShiftAPIVersion() { return typeMapper.getPreferedVersionFor(OS_API_ENDPOINT); } @Override public URL getBaseURL() { return this.baseUrl; } @Override public IAuthorizationContext getAuthorizationContext() { return this.authContext; } public void setToken(String token) { this.authContext.setToken(token); } public String getToken() { return getAuthorizationContext().getToken(); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((baseUrl == null) ? 0 : baseUrl.toString().hashCode()); result = prime * result + ((kubernetesVersion == null) ? 0 : kubernetesVersion.hashCode()); result = prime * result + ((openShiftVersion == null) ? 0 : openShiftVersion.hashCode()); result = prime * result + ((authContext == null || authContext.getToken() == null) ? 0 : authContext.getToken().hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (!(obj instanceof DefaultClient)) return false; DefaultClient other = (DefaultClient) obj; if (baseUrl == null) { if (other.baseUrl != null) return false; } else if (!baseUrl.toString().equals(other.baseUrl.toString())) return false; if (kubernetesVersion == null) { if (other.kubernetesVersion != null) return false; } else if (!kubernetesVersion.equals(other.kubernetesVersion)) return false; if (openShiftVersion == null) { if (other.openShiftVersion != null) return false; } else if (!openShiftVersion.equals(other.openShiftVersion)) { return false; } if (authContext == null) { return other.authContext == null; } else { if (other.authContext == null) { return false; } return ObjectUtils.equals(authContext.getUserName(), other.authContext.getUserName()); } } @SuppressWarnings("unchecked") @Override public <T> T adapt(Class<T> klass) { if(DefaultClient.class.equals(klass)){ return (T) this; } if(OkHttpClient.class.equals(klass)) { return (T) this.client; } if(IApiTypeMapper.class.equals(klass)) { return (T)this.typeMapper; } if(ICapability.class.isAssignableFrom(klass) && this.supports((Class<? extends ICapability>) klass)) { return (T) getCapability((Class<? extends ICapability>)klass); } return null; } }