From fd427eff5010c677dd70d68f401cc54844f57e59 Mon Sep 17 00:00:00 2001 From: Dustin Jenkins Date: Thu, 10 Oct 2024 19:32:04 +0000 Subject: [PATCH 01/15] feat: start of adding image registry user information --- deployment/helm/skaha/CHANGELOG.md | 5 +- deployment/helm/skaha/Chart.yaml | 4 +- deployment/helm/skaha/values.yaml | 2 +- skaha/VERSION | 2 +- .../java/org/opencadc/skaha/SkahaAction.java | 289 +++++++++--------- .../opencadc/skaha/session/PostAction.java | 7 +- .../skaha/utils/CommandExecutioner.java | 57 +--- 7 files changed, 169 insertions(+), 197 deletions(-) diff --git a/deployment/helm/skaha/CHANGELOG.md b/deployment/helm/skaha/CHANGELOG.md index 7962a30e..27ea4cbc 100644 --- a/deployment/helm/skaha/CHANGELOG.md +++ b/deployment/helm/skaha/CHANGELOG.md @@ -1,4 +1,7 @@ -# CHANGELOG for Skaha User Session API (Chart 0.7.6) +# CHANGELOG for Skaha User Session API (Chart 0.8.0) + +## 2024.10.10 (0.8.0) +- Add `x-cadc-registry-secret` request header support to set Harbor CLI secret (or other Image Registry secret) ## 2024.10.07 (0.7.3) - Fix for security context in image caching job diff --git a/deployment/helm/skaha/Chart.yaml b/deployment/helm/skaha/Chart.yaml index cd083fb4..cf4c32d1 100644 --- a/deployment/helm/skaha/Chart.yaml +++ b/deployment/helm/skaha/Chart.yaml @@ -15,13 +15,13 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.7.7 +version: 0.8.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "0.21.2" +appVersion: "0.22.0" dependencies: - name: "redis" diff --git a/deployment/helm/skaha/values.yaml b/deployment/helm/skaha/values.yaml index 51d53ed7..3aee42dd 100644 --- a/deployment/helm/skaha/values.yaml +++ b/deployment/helm/skaha/values.yaml @@ -17,7 +17,7 @@ skahaWorkload: deployment: hostname: myhost.example.com # Change this! skaha: - image: images.opencadc.org/platform/skaha:0.21.2 + image: images.opencadc.org/platform/skaha:0.22.0 imagePullPolicy: Always # Cron string for the image caching cron job schedule. Defaults to every minute. diff --git a/skaha/VERSION b/skaha/VERSION index c0b3e9ff..982a6478 100644 --- a/skaha/VERSION +++ b/skaha/VERSION @@ -1,6 +1,6 @@ ## deployable containers have a semantic and build tag # version tag: major.minor.patch # build version tag: timestamp -VER=0.21.2 +VER=0.22.0 TAGS="${VER} ${VER}-$(date -u +"%Y%m%dT%H%M%S")" unset VER diff --git a/skaha/src/main/java/org/opencadc/skaha/SkahaAction.java b/skaha/src/main/java/org/opencadc/skaha/SkahaAction.java index e097938c..32aaac48 100644 --- a/skaha/src/main/java/org/opencadc/skaha/SkahaAction.java +++ b/skaha/src/main/java/org/opencadc/skaha/SkahaAction.java @@ -1,74 +1,81 @@ /* -************************************************************************ -******************* CANADIAN ASTRONOMY DATA CENTRE ******************* -************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** -* -* (c) 2020. (c) 2020. -* Government of Canada Gouvernement du Canada -* National Research Council Conseil national de recherches -* Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 -* All rights reserved Tous droits réservés -* -* NRC disclaims any warranties, Le CNRC dénie toute garantie -* expressed, implied, or énoncée, implicite ou légale, -* statutory, of any kind with de quelque nature que ce -* respect to the software, soit, concernant le logiciel, -* including without limitation y compris sans restriction -* any warranty of merchantability toute garantie de valeur -* or fitness for a particular marchande ou de pertinence -* purpose. NRC shall not be pour un usage particulier. -* liable in any event for any Le CNRC ne pourra en aucun cas -* damages, whether direct or être tenu responsable de tout -* indirect, special or general, dommage, direct ou indirect, -* consequential or incidental, particulier ou général, -* arising from the use of the accessoire ou fortuit, résultant -* software. Neither the name de l'utilisation du logiciel. Ni -* of the National Research le nom du Conseil National de -* Council of Canada nor the Recherches du Canada ni les noms -* names of its contributors may de ses participants ne peuvent -* be used to endorse or promote être utilisés pour approuver ou -* products derived from this promouvoir les produits dérivés -* software without specific prior de ce logiciel sans autorisation -* written permission. préalable et particulière -* par écrit. -* -* This file is part of the Ce fichier fait partie du projet -* OpenCADC project. OpenCADC. -* -* OpenCADC is free software: OpenCADC est un logiciel libre ; -* you can redistribute it and/or vous pouvez le redistribuer ou le -* modify it under the terms of modifier suivant les termes de -* the GNU Affero General Public la “GNU Affero General Public -* License as published by the License” telle que publiée -* Free Software Foundation, par la Free Software Foundation -* either version 3 of the : soit la version 3 de cette -* License, or (at your option) licence, soit (à votre gré) -* any later version. toute version ultérieure. -* -* OpenCADC is distributed in the OpenCADC est distribué -* hope that it will be useful, dans l’espoir qu’il vous -* but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE -* without even the implied GARANTIE : sans même la garantie -* warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ -* or FITNESS FOR A PARTICULAR ni d’ADÉQUATION À UN OBJECTIF -* PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence -* General Public License for Générale Publique GNU Affero -* more details. pour plus de détails. -* -* You should have received Vous devriez avoir reçu une -* a copy of the GNU Affero copie de la Licence Générale -* General Public License along Publique GNU Affero avec -* with OpenCADC. If not, see OpenCADC ; si ce n’est -* . pas le cas, consultez : -* . -* -************************************************************************ -*/ + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2020. (c) 2020. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la “GNU Affero General Public + * License as published by the License” telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l’espoir qu’il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d’ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n’est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ package org.opencadc.skaha; +import static java.util.stream.Collectors.toList; +import static org.opencadc.skaha.utils.CommonUtils.isNotEmpty; + import ca.nrc.cadc.ac.Group; -import ca.nrc.cadc.auth.*; +import ca.nrc.cadc.auth.AuthMethod; +import ca.nrc.cadc.auth.AuthenticationUtil; +import ca.nrc.cadc.auth.HttpPrincipal; +import ca.nrc.cadc.auth.NotAuthenticatedException; +import ca.nrc.cadc.auth.PosixPrincipal; import ca.nrc.cadc.net.ResourceNotFoundException; import ca.nrc.cadc.reg.Standards; import ca.nrc.cadc.reg.client.LocalAuthority; @@ -76,6 +83,21 @@ import ca.nrc.cadc.rest.RestAction; import ca.nrc.cadc.util.RsaSignatureGenerator; import ca.nrc.cadc.util.StringUtil; +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.AccessControlException; +import java.security.KeyPair; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import javax.security.auth.Subject; import org.apache.log4j.Logger; import org.json.JSONObject; import org.opencadc.auth.PosixMapperClient; @@ -90,27 +112,9 @@ import org.opencadc.skaha.utils.CommonUtils; import org.opencadc.skaha.utils.RedisCache; -import javax.security.auth.Subject; -import java.io.IOException; -import java.net.URI; -import java.net.URL; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.security.AccessControlException; -import java.security.KeyPair; -import java.util.*; -import java.util.stream.Collectors; - -import static java.util.stream.Collectors.toList; -import static org.opencadc.skaha.utils.CommonUtils.isNotEmpty; - public abstract class SkahaAction extends RestAction { - private static final Logger log = Logger.getLogger(SkahaAction.class); - - private static final String POSIX_MAPPER_RESOURCE_ID_KEY = "skaha.posixmapper.resourceid"; - public static final String SESSION_TYPE_CARTA = "carta"; public static final String SESSION_TYPE_NOTEBOOK = "notebook"; public static final String SESSION_TYPE_DESKTOP = "desktop"; @@ -118,10 +122,16 @@ public abstract class SkahaAction extends RestAction { public static final String SESSION_TYPE_HEADLESS = "headless"; public static final String TYPE_DESKTOP_APP = "desktop-app"; public static final String X_AUTH_TOKEN_SKAHA = "x-auth-token-skaha"; + private static final String X_REGISTRY_AUTH_HEADER = "x-skaha-registry-auth"; + private static final Logger log = Logger.getLogger(SkahaAction.class); + private static final String POSIX_MAPPER_RESOURCE_ID_KEY = "skaha.posixmapper.resourceid"; public static List SESSION_TYPES = Arrays.asList( - SESSION_TYPE_CARTA, SESSION_TYPE_NOTEBOOK, SESSION_TYPE_DESKTOP, - SESSION_TYPE_CONTRIB, SESSION_TYPE_HEADLESS, TYPE_DESKTOP_APP); - + SESSION_TYPE_CARTA, SESSION_TYPE_NOTEBOOK, SESSION_TYPE_DESKTOP, + SESSION_TYPE_CONTRIB, SESSION_TYPE_HEADLESS, TYPE_DESKTOP_APP); + protected final PosixMapperConfiguration posixMapperConfiguration; + private final String redisHost; + private final String redisPort; + public List harborHosts = new ArrayList<>(); protected PosixPrincipal posixPrincipal; protected boolean adminUser = false; protected boolean headlessUser = false; @@ -130,8 +140,7 @@ public abstract class SkahaAction extends RestAction { protected String homedir; protected String scratchdir; protected String skahaTld; - protected boolean gpuEnabled; - public List harborHosts = new ArrayList<>(); + protected boolean gpuEnabled; protected String skahaUsersGroup; protected String skahaHeadlessGroup; protected String skahaPriorityHeadlessGroup; @@ -139,13 +148,7 @@ public abstract class SkahaAction extends RestAction { protected String skahaHeadlessPriortyClass; protected int maxUserSessions; protected String skahaPosixCacheURL; - protected final PosixMapperConfiguration posixMapperConfiguration; - - protected RedisCache redis; - private final String redisHost; - private final String redisPort; - protected boolean skahaCallbackFlow = false; protected String callbackSupplementalGroups = null; @@ -155,7 +158,7 @@ public SkahaAction() { homedir = System.getenv("skaha.homedir"); skahaTld = System.getenv("SKAHA_TLD"); gpuEnabled = Boolean.parseBoolean(System.getenv("GPU_ENABLED")); - + scratchdir = System.getenv("skaha.scratchdir"); String harborHostList = System.getenv("skaha.harborhosts"); if (harborHostList == null) { @@ -209,34 +212,6 @@ public SkahaAction() { } } - @Override - protected InlineContentHandler getInlineContentHandler() { - return null; - } - - protected void initRequest() throws Exception { - if (skahaUsersGroup == null) { - throw new IllegalStateException("skaha.usersgroup not defined in system properties"); - } - - URI skahaUsersUri = URI.create(skahaUsersGroup); - final Subject currentSubject = AuthenticationUtil.getCurrentSubject(); - log.debug("Subject: " + currentSubject); - redis = new RedisCache(redisHost, redisPort); - if (isSkahaCallBackFlow(currentSubject)) { - initiateSkahaCallbackFlow(currentSubject, skahaUsersUri); - } else { - initiateGeneralFlow(currentSubject, skahaUsersUri); - } - } - - private boolean isSkahaCallBackFlow(Subject currentSubject) { - AuthMethod authMethod = AuthenticationUtil.getAuthMethodFromCredentials(currentSubject); - log.debug("authMethod is " + authMethod); - log.debug("x-auth-token-skaha is " + syncInput.getHeader(X_AUTH_TOKEN_SKAHA)); - return authMethod == AuthMethod.ANON && isNotEmpty(syncInput.getHeader(X_AUTH_TOKEN_SKAHA)); - } - protected static TokenTool getTokenTool() throws Exception { final EncodedKeyPair encodedKeyPair = getPreAuthorizedTokenSecret(); return new TokenTool(encodedKeyPair.encodedPublicKey, encodedKeyPair.encodedPrivateKey); @@ -256,11 +231,11 @@ private static EncodedKeyPair getPreAuthorizedTokenSecret() throws Exception { // create new secret final String[] createCmd = new String[] { - "kubectl", "--namespace", K8SUtil.getWorkloadNamespace(), "create", "secret", "generic", - K8SUtil.getPreAuthorizedTokenSecretName(), - String.format("--from-literal=%s=", publicKeyPropertyName) + "kubectl", "--namespace", K8SUtil.getWorkloadNamespace(), "create", "secret", "generic", + K8SUtil.getPreAuthorizedTokenSecretName(), + String.format("--from-literal=%s=", publicKeyPropertyName) + CommonUtils.encodeBase64(encodedPublicKey), - String.format("--from-literal=%s=", privateKeyPropertyName) + String.format("--from-literal=%s=", privateKeyPropertyName) + CommonUtils.encodeBase64(encodedPrivateKey) }; @@ -272,12 +247,44 @@ private static EncodedKeyPair getPreAuthorizedTokenSecret() throws Exception { final Base64.Decoder base64Decoder = Base64.getDecoder(); // Decode twice since Kubernetes does a separate Base64 encoding. return new EncodedKeyPair(base64Decoder.decode(base64Decoder.decode( - secretData.getString(publicKeyPropertyName))), + secretData.getString(publicKeyPropertyName))), base64Decoder.decode(base64Decoder.decode( - secretData.getString(privateKeyPropertyName)))); + secretData.getString(privateKeyPropertyName)))); } } + @Override + protected InlineContentHandler getInlineContentHandler() { + return null; + } + + protected void initRequest() throws Exception { + if (skahaUsersGroup == null) { + throw new IllegalStateException("skaha.usersgroup not defined in system properties"); + } + + URI skahaUsersUri = URI.create(skahaUsersGroup); + final Subject currentSubject = AuthenticationUtil.getCurrentSubject(); + log.debug("Subject: " + currentSubject); + redis = new RedisCache(redisHost, redisPort); + if (isSkahaCallBackFlow(currentSubject)) { + initiateSkahaCallbackFlow(currentSubject, skahaUsersUri); + } else { + initiateGeneralFlow(currentSubject, skahaUsersUri); + } + } + + protected String getRegistryAuth() { + return this.syncInput.getHeader(SkahaAction.X_REGISTRY_AUTH_HEADER); + } + + private boolean isSkahaCallBackFlow(Subject currentSubject) { + AuthMethod authMethod = AuthenticationUtil.getAuthMethodFromCredentials(currentSubject); + log.debug("authMethod is " + authMethod); + log.debug("x-auth-token-skaha is " + syncInput.getHeader(X_AUTH_TOKEN_SKAHA)); + return authMethod == AuthMethod.ANON && isNotEmpty(syncInput.getHeader(X_AUTH_TOKEN_SKAHA)); + } + private void initiateSkahaCallbackFlow(Subject currentSubject, URI skahaUsersUri) { skahaCallbackFlow = true; final String xAuthTokenSkaha = syncInput.getHeader(X_AUTH_TOKEN_SKAHA); @@ -303,7 +310,7 @@ private void initiateSkahaCallbackFlow(Subject currentSubject, URI skahaUsersUri } private void initiateGeneralFlow(Subject currentSubject, URI skahaUsersUri) - throws IOException, InterruptedException, ResourceNotFoundException { + throws IOException, InterruptedException, ResourceNotFoundException { GroupURI skahaUsersGroupUri = new GroupURI(skahaUsersUri); if (currentSubject == null || currentSubject.getPrincipals().isEmpty()) { throw new NotAuthenticatedException("Unauthorized"); @@ -337,16 +344,16 @@ private void initiateGeneralFlow(Subject currentSubject, URI skahaUsersUri) Set skahaUsersGroupUriSet = ivoaGroupClient.getMemberships(gmsSearchURI); final GroupURI skahaHeadlessGroupURI = StringUtil.hasText(this.skahaHeadlessGroup) - ? new GroupURI(URI.create(this.skahaHeadlessGroup)) - : null; + ? new GroupURI(URI.create(this.skahaHeadlessGroup)) + : null; if (skahaHeadlessGroupURI != null && skahaUsersGroupUriSet.contains(skahaHeadlessGroupURI)) { headlessUser = true; } final GroupURI skahaPriorityHeadlessGroupURI = StringUtil.hasText(this.skahaPriorityHeadlessGroup) - ? new GroupURI(URI.create(this.skahaPriorityHeadlessGroup)) - : null; + ? new GroupURI(URI.create(this.skahaPriorityHeadlessGroup)) + : null; if (skahaPriorityHeadlessGroupURI != null && skahaUsersGroupUriSet.contains(skahaPriorityHeadlessGroupURI)) { priorityHeadlessUser = true; @@ -372,8 +379,8 @@ private void initiateGeneralFlow(Subject currentSubject, URI skahaUsersUri) } List groups = isNotEmpty(skahaUsersGroupUriSet) - ? skahaUsersGroupUriSet.stream().map(Group::new).collect(toList()) - : Collections.emptyList(); + ? skahaUsersGroupUriSet.stream().map(Group::new).collect(toList()) + : Collections.emptyList(); // adding all groups to the Subject currentSubject.getPublicCredentials().add(groups); @@ -399,9 +406,9 @@ public Image getImage(String imageID) throws Exception { return null; } return images.parallelStream() - .filter(image -> image.getId().equals(imageID)) - .findFirst() - .orElse(null); + .filter(image -> image.getId().equals(imageID)) + .findFirst() + .orElse(null); } /** @@ -421,7 +428,7 @@ protected PosixMapperConfiguration(final URI configuredPosixMapperID) throws IOE baseURL = configuredPosixMapperID.toURL(); } else { throw new IllegalStateException("Incorrect configuration for specified posix mapper service (" - + configuredPosixMapperID + ")."); + + configuredPosixMapperID + ")."); } } diff --git a/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java b/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java index 8f251bdb..010339a0 100644 --- a/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java +++ b/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java @@ -3,7 +3,7 @@ ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** * - * (c) 2020. (c) 2020. + * (c) 2024. (c) 2024. * Government of Canada Gouvernement du Canada * National Research Council Conseil national de recherches * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 @@ -69,15 +69,12 @@ import ca.nrc.cadc.ac.Group; import ca.nrc.cadc.auth.AuthenticationUtil; -import ca.nrc.cadc.net.HttpGet; import ca.nrc.cadc.net.ResourceNotFoundException; import ca.nrc.cadc.reg.client.LocalAuthority; import ca.nrc.cadc.reg.client.RegistryClient; import ca.nrc.cadc.util.StringUtil; import ca.nrc.cadc.uws.server.RandomStringGenerator; import org.apache.log4j.Logger; -import org.json.JSONObject; -import org.json.JSONTokener; import org.opencadc.auth.PosixGroup; import org.opencadc.gms.GroupURI; import org.opencadc.permissions.WriteGrant; @@ -91,7 +88,6 @@ import java.io.IOException; import java.io.OutputStream; import java.net.URI; -import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -101,7 +97,6 @@ import java.time.temporal.ChronoUnit; import java.util.*; import java.util.stream.Collectors; -import org.opencadc.skaha.utils.CommandExecutioner; import org.opencadc.skaha.utils.PosixCache; import static org.opencadc.skaha.utils.CommandExecutioner.execute; diff --git a/skaha/src/main/java/org/opencadc/skaha/utils/CommandExecutioner.java b/skaha/src/main/java/org/opencadc/skaha/utils/CommandExecutioner.java index e3c20557..e712df78 100644 --- a/skaha/src/main/java/org/opencadc/skaha/utils/CommandExecutioner.java +++ b/skaha/src/main/java/org/opencadc/skaha/utils/CommandExecutioner.java @@ -10,9 +10,8 @@ import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; import java.nio.charset.StandardCharsets; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Arrays; +import org.opencadc.skaha.K8SUtil; public class CommandExecutioner { private static final Logger log = Logger.getLogger(CommandExecutioner.class); @@ -21,26 +20,6 @@ public static String execute(String[] command) throws IOException, InterruptedEx return execute(command, true); } - public static String executeInShell(String[] command, boolean allowError) throws IOException, InterruptedException { - final ProcessBuilder processBuilder = new ProcessBuilder("/bin/sh", "-c", String.join(" ", command)); - final Process p = processBuilder.start(); - final String stdout = readStream(p.getInputStream()); - final String stderr = readStream(p.getErrorStream()); - log.debug("stdout: " + stdout); - log.debug("stderr: " + stderr); - int status = p.waitFor(); - log.debug("Status=" + status + " for command: " + Arrays.toString(command)); - if (status != 0) { - if (allowError) { - return stderr; - } else { - String message = "Error executing command: " + Arrays.toString(command) + " Error: " + stderr; - throw new IOException(message); - } - } - return stdout.trim(); - } - public static String execute(String[] command, boolean allowError) throws IOException, InterruptedException { Process p = Runtime.getRuntime().exec(command); String stdout = readStream(p.getInputStream()); @@ -102,6 +81,17 @@ public static void execute(final String[] command, final OutputStream standardOu } } + protected static void ensureRegistrySecret(final String registryHost, final String registryUsername, final String secret, final String secretName) { + // create new secret + String[] createCmd = new String[] { + "kubectl", "--namespace", K8SUtil.getWorkloadNamespace(), "create", "secret", "docker-registry", + secretName, + "--docker-server=" + registryHost, + "--docker-username=" + registryUsername, + "--docker-password=" + secret + }; + } + public static JSONObject getSecretData(final String secretName, final String secretNamespace) throws Exception { // Check the current secret final String[] getSecretCommand = new String[] { @@ -127,29 +117,6 @@ protected static String readStream(InputStream in) throws IOException { return buffer.toString(StandardCharsets.UTF_8); } - public static String createDirectoryIfNotExist(String... paths) { - Path path = Paths.get("/", paths); - File directory = new File(path.toString()); - if (!(directory.exists())) { - directory.mkdir(); - } - return path.toString(); - } - - public static String createOrOverrideFile(String directoryPath, String fileName, String content) - throws IOException { - Path path = Paths.get(directoryPath, fileName); - File file = new File(path.toString()); - if (!(file.exists())) { - file.createNewFile(); - } - BufferedWriter writer = new BufferedWriter(new FileWriter(file)); - writer.write(content + "\n"); - writer.flush(); - writer.close(); - return path.toString(); - } - public static void changeOwnership(String path, int posixId, int groupId) throws IOException, InterruptedException { String[] chown = new String[]{"chown", posixId + ":" + groupId, path}; execute(chown); From b7ff4966812624be430bce811f18366e9a58e054 Mon Sep 17 00:00:00 2001 From: Dustin Jenkins Date: Thu, 10 Oct 2024 23:45:29 +0000 Subject: [PATCH 02/15] fix: first code pass at adding secret --- .../helm/skaha/skaha-config/launch-carta.yaml | 2 ++ .../java/org/opencadc/skaha/SkahaAction.java | 2 +- .../opencadc/skaha/session/PostAction.java | 31 ++++++++++++++++++- .../skaha/utils/CommandExecutioner.java | 2 +- 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/deployment/helm/skaha/skaha-config/launch-carta.yaml b/deployment/helm/skaha/skaha-config/launch-carta.yaml index 065fae8a..b2b11a4e 100644 --- a/deployment/helm/skaha/skaha-config/launch-carta.yaml +++ b/deployment/helm/skaha/skaha-config/launch-carta.yaml @@ -28,6 +28,8 @@ spec: enableServiceLinks: false restartPolicy: OnFailure ${skaha.schedulegpu} + imagePullSecrets: + - name: ${software.imagesecret} securityContext: {{ template "skaha.job.securityContext" . }} priorityClassName: uber-user-preempt-medium diff --git a/skaha/src/main/java/org/opencadc/skaha/SkahaAction.java b/skaha/src/main/java/org/opencadc/skaha/SkahaAction.java index 32aaac48..33974637 100644 --- a/skaha/src/main/java/org/opencadc/skaha/SkahaAction.java +++ b/skaha/src/main/java/org/opencadc/skaha/SkahaAction.java @@ -398,7 +398,7 @@ protected int getUID() { return posixPrincipal.getUidNumber(); } - public Image getImage(String imageID) throws Exception { + public Image getImage(String imageID) { log.debug("get image: " + imageID); List images = redis.getAll("public", Image.class); if (images == null) { diff --git a/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java b/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java index 010339a0..2c9fefd5 100644 --- a/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java +++ b/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java @@ -97,6 +97,7 @@ import java.time.temporal.ChronoUnit; import java.util.*; import java.util.stream.Collectors; +import org.opencadc.skaha.utils.CommandExecutioner; import org.opencadc.skaha.utils.PosixCache; import static org.opencadc.skaha.utils.CommandExecutioner.execute; @@ -141,12 +142,13 @@ public class PostAction extends SessionAction { public static final String HEADLESS_PRIORITY = "headless.priority"; public static final String HEADLESS_IMAGE_BUNDLE = "headless.image.bundle"; private static final String CREATE_USER_BASE_COMMAND = "/usr/local/bin/add-user"; - private static final String DEFAULT_HARBOR_SECRET = "notused"; private static final String DESKTOP_SESSION_APP_TOKEN = "software.desktop.app.token"; private static final String POSIX_MAPPER_URI = "POSIX_MAPPER_URI"; private static final String REGISTRY_URL = "REGISTRY_URL"; private static final String SKAHA_TLD = "SKAHA_TLD"; + public static final String DEFAULT_SOFTWARE_IMAGESECRET_VALUE = "notused"; + public PostAction() { super(); } @@ -618,6 +620,8 @@ public void createSession(String type, String image, String name, Integer cores, new LocalAuthority().getServiceURI(RegistryClient.class.getName() + ".baseURL").toString()); jobLaunchString = setConfigValue(jobLaunchString, SKAHA_TLD, skahaTld); + jobLaunchString = setConfigValue(jobLaunchString, PostAction.SOFTWARE_IMAGESECRET, createRegistryImageSecret(image)); + if (type.equals(SessionAction.SESSION_TYPE_DESKTOP)) { jobLaunchString = setConfigValue(jobLaunchString, PostAction.DESKTOP_SESSION_APP_TOKEN, generateToken()); } @@ -665,6 +669,31 @@ private String generateToken() throws Exception { return SkahaAction.getTokenTool().generateToken(URI.create(this.skahaUsersGroup), WriteGrant.class, this.sessionID); } + /** + * Create a registry secret and return its name. + * @param imageID The image ID to create a secret for. + * @return String secret name, never null. + */ + private String createRegistryImageSecret(final String imageID) { + final String registryAuth = getRegistryAuth(); + final String username = this.posixPrincipal.username; + final String secretName = "harbor-secret-" + username.toLowerCase(); + CommandExecutioner.ensureRegistrySecret(getRegistryHost(imageID), username, + StringUtil.hasText(registryAuth) ? registryAuth : PostAction.DEFAULT_SOFTWARE_IMAGESECRET_VALUE, + secretName); + + return secretName; + } + + private String getRegistryHost(final String imageID) { + final String registryHost = this.harborHosts.stream().filter(imageID::startsWith).findFirst().orElse(null); + if (registryHost == null) { + throw new IllegalArgumentException("not a skaha harbor image: " + imageID); + } + + return registryHost; + } + /** * Attach a desktop application. * TODO: This method requires rework. The Job Name does not use the same mechanism as the K8SUtil.getJobName() diff --git a/skaha/src/main/java/org/opencadc/skaha/utils/CommandExecutioner.java b/skaha/src/main/java/org/opencadc/skaha/utils/CommandExecutioner.java index e712df78..42848c9f 100644 --- a/skaha/src/main/java/org/opencadc/skaha/utils/CommandExecutioner.java +++ b/skaha/src/main/java/org/opencadc/skaha/utils/CommandExecutioner.java @@ -81,7 +81,7 @@ public static void execute(final String[] command, final OutputStream standardOu } } - protected static void ensureRegistrySecret(final String registryHost, final String registryUsername, final String secret, final String secretName) { + public static void ensureRegistrySecret(final String registryHost, final String registryUsername, final String secret, final String secretName) { // create new secret String[] createCmd = new String[] { "kubectl", "--namespace", K8SUtil.getWorkloadNamespace(), "create", "secret", "docker-registry", From 3a06b2af919698c588b162eecda1b8833fdc3361 Mon Sep 17 00:00:00 2001 From: Dustin Jenkins Date: Fri, 11 Oct 2024 22:43:14 +0000 Subject: [PATCH 03/15] feat: add ability to specify registry credentials in header --- .../start-software-sh.template | 10 +- .../skaha-config/launch-contributed.yaml | 2 + .../skaha-config/launch-desktop-app.yaml | 2 + .../skaha/skaha-config/launch-desktop.yaml | 4 + .../skaha/skaha-config/launch-headless.yaml | 2 + .../skaha/skaha-config/launch-notebook.yaml | 2 + .../java/org/opencadc/skaha/SessionUtil.java | 6 - .../java/org/opencadc/skaha/SkahaAction.java | 12 +- .../java/org/opencadc/skaha/image/Image.java | 4 +- .../skaha/registry/ImageRegistryAuth.java | 72 +++++ .../opencadc/skaha/session/PostAction.java | 247 ++++++++++-------- .../opencadc/skaha/session/SessionAction.java | 26 +- .../skaha/utils/CommandExecutioner.java | 56 +++- .../org/opencadc/skaha/utils/PosixCache.java | 1 + 14 files changed, 292 insertions(+), 154 deletions(-) create mode 100644 skaha/src/main/java/org/opencadc/skaha/registry/ImageRegistryAuth.java diff --git a/deployment/helm/skaha/desktop-template/start-software-sh.template b/deployment/helm/skaha/desktop-template/start-software-sh.template index 59b5e398..28e79947 100644 --- a/deployment/helm/skaha/desktop-template/start-software-sh.template +++ b/deployment/helm/skaha/desktop-template/start-software-sh.template @@ -3,6 +3,8 @@ HOST=(HOST) # Callback token TOKEN="${DESKTOP_SESSION_APP_TOKEN}" +REGISTRY_AUTH="${DESKTOP_SESSION_APP_REGISTRY_AUTH}" +CURL_APP_SESSION_HEADERS="--header \"x-auth-token-skaha: ${TOKEN}\" --header \"x-skaha-registry-auth: ${REGISTRY_AUTH}\"" handle_error() { echo "$1" @@ -89,7 +91,7 @@ prompt_user() { if [ -z "${TOKEN}" ]; then handle_error "[skaha] No credentials to call back to Skaha with." else - app_id=`curl -s -L -k --header "x-auth-token-skaha: ${TOKEN}" -d "image=(IMAGE_ID)" --data-urlencode "param=(NAME)" https://(HOST)/skaha/(SKAHA_API_VERSION)/session/${VNC_PW}/app` + app_id=`curl -s -L -k "${CURL_APP_SESSION_HEADERS}" -d "image=(IMAGE_ID)" --data-urlencode "param=(NAME)" https://(HOST)/skaha/(SKAHA_API_VERSION)/session/${VNC_PW}/app` fi break elif [[ ${yn} == "y" || ${yn} == "Y" ]]; then @@ -101,7 +103,7 @@ prompt_user() { if [ -z "${TOKEN}" ]; then handle_error "[skaha] No credentials to call back to Skaha with." else - app_id=`curl -s -L -k --header "x-auth-token-skaha: ${TOKEN}" -d "cores=${cores}" -d "ram=$ram" -d "image=(IMAGE_ID)" --data-urlencode "param=(NAME)" https://(HOST)/skaha/(SKAHA_API_VERSION)/session/${VNC_PW}/app` + app_id=`curl -s -L -k "${CURL_APP_SESSION_HEADERS}" -d "cores=${cores}" -d "ram=$ram" -d "image=(IMAGE_ID)" --data-urlencode "param=(NAME)" https://(HOST)/skaha/(SKAHA_API_VERSION)/session/${VNC_PW}/app` fi break else @@ -123,7 +125,7 @@ get_status() { if [ -z "${TOKEN}" ]; then handle_error "[skaha] No credentials to call back to Skaha with." else - curl_out=`curl -s -L -k --header "x-auth-token-skaha: ${TOKEN}" https://(HOST)/skaha/(SKAHA_API_VERSION)/session/${VNC_PW}/app/$1` + curl_out=`curl -s -L -k "${CURL_APP_SESSION_HEADERS}" https://(HOST)/skaha/(SKAHA_API_VERSION)/session/${VNC_PW}/app/$1` fi while [[ ${curl_out} != *"status"* ]]; do @@ -135,7 +137,7 @@ get_status() { sleep 1 # No need to check for the Token again as it passed above. - curl_out=`curl -s -L -k --header "x-auth-token-skaha: ${TOKEN}" https://(HOST)/skaha/(SKAHA_API_VERSION)/session/${VNC_PW}/app/$1` + curl_out=`curl -s -L -k "${CURL_APP_SESSION_HEADERS}" https://(HOST)/skaha/(SKAHA_API_VERSION)/session/${VNC_PW}/app/$1` done } diff --git a/deployment/helm/skaha/skaha-config/launch-contributed.yaml b/deployment/helm/skaha/skaha-config/launch-contributed.yaml index bb255b0d..addaad90 100644 --- a/deployment/helm/skaha/skaha-config/launch-contributed.yaml +++ b/deployment/helm/skaha/skaha-config/launch-contributed.yaml @@ -28,6 +28,8 @@ spec: enableServiceLinks: false restartPolicy: OnFailure ${skaha.schedulegpu} + imagePullSecrets: + - name: ${software.imagesecret} securityContext: {{ template "skaha.job.securityContext" . }} priorityClassName: uber-user-preempt-medium diff --git a/deployment/helm/skaha/skaha-config/launch-desktop-app.yaml b/deployment/helm/skaha/skaha-config/launch-desktop-app.yaml index 2cef3f5d..a8cb534c 100644 --- a/deployment/helm/skaha/skaha-config/launch-desktop-app.yaml +++ b/deployment/helm/skaha/skaha-config/launch-desktop-app.yaml @@ -28,6 +28,8 @@ spec: enableServiceLinks: false restartPolicy: OnFailure ${skaha.schedulegpu} + imagePullSecrets: + - name: ${software.imagesecret} securityContext: {{ template "skaha.job.securityContext" . }} priorityClassName: uber-user-preempt-medium diff --git a/deployment/helm/skaha/skaha-config/launch-desktop.yaml b/deployment/helm/skaha/skaha-config/launch-desktop.yaml index 1966a698..6af7bdce 100644 --- a/deployment/helm/skaha/skaha-config/launch-desktop.yaml +++ b/deployment/helm/skaha/skaha-config/launch-desktop.yaml @@ -28,6 +28,8 @@ spec: enableServiceLinks: false restartPolicy: OnFailure ${skaha.schedulegpu} + imagePullSecrets: + - name: ${software.imagesecret} securityContext: {{ template "skaha.job.securityContext" . }} priorityClassName: uber-user-preempt-medium @@ -56,6 +58,8 @@ spec: value: "v0" - name: DESKTOP_SESSION_APP_TOKEN value: "${software.desktop.app.token}" + - name: DESKTOP_SESSION_APP_REGISTRY_AUTH + value: "${software.desktop.app.registry-auth}" securityContext: privileged: false allowPrivilegeEscalation: false diff --git a/deployment/helm/skaha/skaha-config/launch-headless.yaml b/deployment/helm/skaha/skaha-config/launch-headless.yaml index 5a89326a..dec8f63f 100644 --- a/deployment/helm/skaha/skaha-config/launch-headless.yaml +++ b/deployment/helm/skaha/skaha-config/launch-headless.yaml @@ -29,6 +29,8 @@ spec: enableServiceLinks: false restartPolicy: Never ${skaha.schedulegpu} + imagePullSecrets: + - name: ${software.imagesecret} hostname: "${software.hostname}" initContainers: {{ template "skaha.job.initContainers" . }} diff --git a/deployment/helm/skaha/skaha-config/launch-notebook.yaml b/deployment/helm/skaha/skaha-config/launch-notebook.yaml index b0eb83bc..624eab57 100644 --- a/deployment/helm/skaha/skaha-config/launch-notebook.yaml +++ b/deployment/helm/skaha/skaha-config/launch-notebook.yaml @@ -28,6 +28,8 @@ spec: enableServiceLinks: false restartPolicy: OnFailure ${skaha.schedulegpu} + imagePullSecrets: + - name: ${software.imagesecret} securityContext: {{ template "skaha.job.securityContext" . }} priorityClassName: uber-user-preempt-medium diff --git a/skaha/src/intTest/java/org/opencadc/skaha/SessionUtil.java b/skaha/src/intTest/java/org/opencadc/skaha/SessionUtil.java index 58650d2c..31e369b0 100644 --- a/skaha/src/intTest/java/org/opencadc/skaha/SessionUtil.java +++ b/skaha/src/intTest/java/org/opencadc/skaha/SessionUtil.java @@ -430,12 +430,6 @@ static void verifySession(final Session session, final String expectedSessionTyp } } - protected static List getSessionsOfType(final URL sessionURL, final String type, String... omitStatuses) throws Exception { - return SessionUtil.getSessions(sessionURL, omitStatuses).stream() - .filter(session -> session.getType().equals(type)) - .collect(Collectors.toList()); - } - private static List getAllSessions(final URL sessionURL) throws Exception { final HttpGet get = new HttpGet(sessionURL, true); get.prepare(); diff --git a/skaha/src/main/java/org/opencadc/skaha/SkahaAction.java b/skaha/src/main/java/org/opencadc/skaha/SkahaAction.java index 33974637..ec74e5f7 100644 --- a/skaha/src/main/java/org/opencadc/skaha/SkahaAction.java +++ b/skaha/src/main/java/org/opencadc/skaha/SkahaAction.java @@ -106,6 +106,7 @@ import org.opencadc.permissions.TokenTool; import org.opencadc.permissions.WriteGrant; import org.opencadc.skaha.image.Image; +import org.opencadc.skaha.registry.ImageRegistryAuth; import org.opencadc.skaha.session.Session; import org.opencadc.skaha.session.SessionDAO; import org.opencadc.skaha.utils.CommandExecutioner; @@ -274,8 +275,13 @@ protected void initRequest() throws Exception { } } - protected String getRegistryAuth() { - return this.syncInput.getHeader(SkahaAction.X_REGISTRY_AUTH_HEADER); + protected ImageRegistryAuth getRegistryAuth(final String registryHost) { + final String registryAuthValue = this.syncInput.getHeader(SkahaAction.X_REGISTRY_AUTH_HEADER); + if (!StringUtil.hasText(registryAuthValue)) { + throw new IllegalArgumentException("No authentication provided for unknown or private image. Use " + + SkahaAction.X_REGISTRY_AUTH_HEADER + " request header with base64Encode(username:secret)."); + } + return ImageRegistryAuth.fromEncoded(registryAuthValue, registryHost); } private boolean isSkahaCallBackFlow(Subject currentSubject) { @@ -398,7 +404,7 @@ protected int getUID() { return posixPrincipal.getUidNumber(); } - public Image getImage(String imageID) { + public Image getPublicImage(String imageID) { log.debug("get image: " + imageID); List images = redis.getAll("public", Image.class); if (images == null) { diff --git a/skaha/src/main/java/org/opencadc/skaha/image/Image.java b/skaha/src/main/java/org/opencadc/skaha/image/Image.java index 3a485d96..2cafaaf9 100644 --- a/skaha/src/main/java/org/opencadc/skaha/image/Image.java +++ b/skaha/src/main/java/org/opencadc/skaha/image/Image.java @@ -87,13 +87,13 @@ public Image() { public Image(String id, Set types, String digest) { if (id == null) { - throw new IllegalArgumentException("id requried"); + throw new IllegalArgumentException("id required"); } if (types == null || types.isEmpty()) { throw new IllegalArgumentException("type required"); } if (digest == null) { - throw new IllegalArgumentException("digest requried"); + throw new IllegalArgumentException("digest required"); } this.id = id; this.types = new HashSet(types); diff --git a/skaha/src/main/java/org/opencadc/skaha/registry/ImageRegistryAuth.java b/skaha/src/main/java/org/opencadc/skaha/registry/ImageRegistryAuth.java new file mode 100644 index 00000000..b7dac013 --- /dev/null +++ b/skaha/src/main/java/org/opencadc/skaha/registry/ImageRegistryAuth.java @@ -0,0 +1,72 @@ +package org.opencadc.skaha.registry; + +import ca.nrc.cadc.util.Base64; +import ca.nrc.cadc.util.StringUtil; + + +/** + * Represents credentials to an image registry. Will be used from the request input as a base64 encoded value. + */ +public class ImageRegistryAuth { + private static final String VALUE_DELIMITER = ":"; + + private final String host; + private final String username; + private final byte[] secret; + + ImageRegistryAuth(final String username, final byte[] secret, final String host) { + if (!StringUtil.hasText(username)) { + throw new IllegalArgumentException("username is required."); + } else if (secret.length == 0) { + throw new IllegalArgumentException("secret value cannot be empty."); + } else if (!StringUtil.hasText(host)) { + throw new IllegalArgumentException("registry host is required."); + } + + this.username = username; + this.secret = secret; + this.host = host; + } + + /** + * Constructor to use the Base64 encoded value to obtain the credentials for an Image Registry. + * + * @param encodedValue The Base64 encoded String. Never null. + * @param host The registry host for these credentials. + * @return ImageRegistryAuth instance. Never null. + */ + public static ImageRegistryAuth fromEncoded(final String encodedValue, final String host) { + if (!StringUtil.hasText(encodedValue)) { + throw new IllegalArgumentException("Encoded auth username and key is required."); + } + + final String decodedValue = new String(Base64.decode(encodedValue)); + final String[] values = decodedValue.split(ImageRegistryAuth.VALUE_DELIMITER); + + if (values.length != 2) { + throw new IllegalArgumentException("Invalid input. Must be in form of username:secret"); + } + + return new ImageRegistryAuth(values[0].trim(), values[1].trim().getBytes(), host); + } + + public String getEncoded() { + return Base64.encodeString(this.username + ImageRegistryAuth.VALUE_DELIMITER + new String(this.secret)); + } + + public String getHost() { + return host; + } + + public String getUsername() { + return username; + } + + public byte[] getSecret() { + return secret; + } + + public boolean isSecretValid() { + return !new String(this.secret).startsWith("\ufffd"); + } +} diff --git a/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java b/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java index 2c9fefd5..7ac65c75 100644 --- a/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java +++ b/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java @@ -67,6 +67,8 @@ package org.opencadc.skaha.session; +import static org.opencadc.skaha.utils.CommandExecutioner.execute; + import ca.nrc.cadc.ac.Group; import ca.nrc.cadc.auth.AuthenticationUtil; import ca.nrc.cadc.net.ResourceNotFoundException; @@ -74,16 +76,6 @@ import ca.nrc.cadc.reg.client.RegistryClient; import ca.nrc.cadc.util.StringUtil; import ca.nrc.cadc.uws.server.RandomStringGenerator; -import org.apache.log4j.Logger; -import org.opencadc.auth.PosixGroup; -import org.opencadc.gms.GroupURI; -import org.opencadc.permissions.WriteGrant; -import org.opencadc.skaha.K8SUtil; -import org.opencadc.skaha.SkahaAction; -import org.opencadc.skaha.context.ResourceContexts; -import org.opencadc.skaha.image.Image; - -import javax.security.auth.Subject; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; @@ -95,24 +87,31 @@ import java.security.AccessControlException; import java.time.Instant; import java.time.temporal.ChronoUnit; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; +import javax.security.auth.Subject; +import org.apache.log4j.Logger; +import org.opencadc.auth.PosixGroup; +import org.opencadc.gms.GroupURI; +import org.opencadc.permissions.WriteGrant; +import org.opencadc.skaha.K8SUtil; +import org.opencadc.skaha.SkahaAction; +import org.opencadc.skaha.context.ResourceContexts; +import org.opencadc.skaha.image.Image; +import org.opencadc.skaha.registry.ImageRegistryAuth; import org.opencadc.skaha.utils.CommandExecutioner; import org.opencadc.skaha.utils.PosixCache; -import static org.opencadc.skaha.utils.CommandExecutioner.execute; - /** * @author majorb */ public class PostAction extends SessionAction { - private static final Logger log = Logger.getLogger(PostAction.class); - - // k8s rejects label size > 63. Since k8s appends a maximum of six characters - // to a job name to form a pod name, we limit the job name length to 57 characters. - private static final int MAX_JOB_NAME_LENGTH = 57; - // variables replaced in kubernetes yaml config files for // launching desktop sessions and launching software // use in the form: ${var.name} @@ -141,18 +140,51 @@ public class PostAction extends SessionAction { public static final String SOFTWARE_LIMITS_GPUS = "software.limits.gpus"; public static final String HEADLESS_PRIORITY = "headless.priority"; public static final String HEADLESS_IMAGE_BUNDLE = "headless.image.bundle"; + public static final String DEFAULT_SOFTWARE_IMAGESECRET_VALUE = "notused"; + private static final Logger log = Logger.getLogger(PostAction.class); + // k8s rejects label size > 63. Since k8s appends a maximum of six characters + // to a job name to form a pod name, we limit the job name length to 57 characters. + private static final int MAX_JOB_NAME_LENGTH = 57; private static final String CREATE_USER_BASE_COMMAND = "/usr/local/bin/add-user"; private static final String DESKTOP_SESSION_APP_TOKEN = "software.desktop.app.token"; + private static final String DESKTOP_SESSION_REGISTRY_AUTH = "software.desktop.app.registry-auth"; private static final String POSIX_MAPPER_URI = "POSIX_MAPPER_URI"; private static final String REGISTRY_URL = "REGISTRY_URL"; private static final String SKAHA_TLD = "SKAHA_TLD"; - public static final String DEFAULT_SOFTWARE_IMAGESECRET_VALUE = "notused"; - public PostAction() { super(); } + private static List getRenewJobNamesCmd(String forUserID, String sessionID) { + final String k8sNamespace = K8SUtil.getWorkloadNamespace(); + List getRenewJobNamesCmd = new ArrayList<>(); + getRenewJobNamesCmd.add("kubectl"); + getRenewJobNamesCmd.add("get"); + getRenewJobNamesCmd.add("--namespace"); + getRenewJobNamesCmd.add(k8sNamespace); + getRenewJobNamesCmd.add("job"); + getRenewJobNamesCmd.add("-l"); + getRenewJobNamesCmd.add("canfar-net-sessionID=" + sessionID + ",canfar-net-userid=" + forUserID); + getRenewJobNamesCmd.add("--no-headers=true"); + getRenewJobNamesCmd.add("-o"); + + String customColumns = "custom-columns=" + + "NAME:.metadata.name," + + "UID:.metadata.uid," + + "STATUS:.status.active," + + "START:.status.startTime"; + + getRenewJobNamesCmd.add(customColumns); + return getRenewJobNamesCmd; + } + + private static Set> getCachedGroupsFromSubject() { + Subject subject = AuthenticationUtil.getCurrentSubject(); + Class> c = (Class>) (Class) List.class; + return subject.getPublicCredentials(c); + } + @Override public void doAction() throws Exception { @@ -169,7 +201,11 @@ public void doAction() throws Exception { if (requestType.equals(REQUEST_TYPE_SESSION)) { if (sessionID == null) { - String type = syncInput.getParameter("type"); + final String requestedType = syncInput.getParameter("type"); + + // Absence of type is assumed to be headless + final String type = StringUtil.hasText(requestedType) ? requestedType : PostAction.SESSION_TYPE_HEADLESS; + validatedType = validateImage(image, type); Integer cores = getCoresParam(); if (cores == null) { @@ -229,7 +265,7 @@ public void doAction() throws Exception { } } else { throw new IllegalArgumentException( - "No active job for user " + posixPrincipal + " with session " + sessionID); + "No active job for user " + posixPrincipal + " with session " + sessionID); } } else { throw new UnsupportedOperationException("unrecognized action"); @@ -238,7 +274,6 @@ public void doAction() throws Exception { throw new UnsupportedOperationException("Cannot modify an existing session"); } } - return; } else if (requestType.equals(REQUEST_TYPE_APP)) { if (appID == null) { // create an app @@ -279,8 +314,8 @@ void allocateUser() throws Exception { log.debug("PostAction.makeUserBase()"); final Path userHomePath = getUserHomeDirectory(); final String[] allocateUserCommand = new String[] { - PostAction.CREATE_USER_BASE_COMMAND, getUsername(), Integer.toString(getUID()), - getDefaultQuota(), userHomePath.toAbsolutePath().toString() + PostAction.CREATE_USER_BASE_COMMAND, getUsername(), Integer.toString(getUID()), + getDefaultQuota(), userHomePath.toAbsolutePath().toString() }; log.debug("Executing " + Arrays.toString(allocateUserCommand)); @@ -293,8 +328,8 @@ PostAction.CREATE_USER_BASE_COMMAND, getUsername(), Integer.toString(getUID()), if (StringUtil.hasText(errorOutput)) { throw new IOException("Unable to create user home." - + "\nError message from server: " + errorOutput - + "\nOutput from command: " + commandOutput); + + "\nError message from server: " + errorOutput + + "\nOutput from command: " + commandOutput); } else { log.debug("PostAction.makeUserBase() success creating: " + commandOutput); } @@ -304,7 +339,7 @@ PostAction.CREATE_USER_BASE_COMMAND, getUsername(), Integer.toString(getUID()), } void executeCommand(final String[] command, final OutputStream standardOut, final OutputStream standardErr) - throws IOException, InterruptedException { + throws IOException, InterruptedException { execute(command, standardOut, standardErr); } @@ -353,7 +388,6 @@ private Integer getRamParam() { return ram; } - private void renew(Map.Entry> entry) throws Exception { Long newExpiryTime = calculateExpiryTime(entry.getValue()); if (newExpiryTime > 0) { @@ -372,7 +406,6 @@ private void renew(Map.Entry> entry) throws Exception { } } - private Long calculateExpiryTime(List jobAttributes) throws Exception { String uid = jobAttributes.get(0); String startTimeStr = jobAttributes.get(1); @@ -433,29 +466,6 @@ private Map> getJobsToRenew(String forUserID, String sessio return renewJobMap; } - private static List getRenewJobNamesCmd(String forUserID, String sessionID) { - final String k8sNamespace = K8SUtil.getWorkloadNamespace(); - List getRenewJobNamesCmd = new ArrayList<>(); - getRenewJobNamesCmd.add("kubectl"); - getRenewJobNamesCmd.add("get"); - getRenewJobNamesCmd.add("--namespace"); - getRenewJobNamesCmd.add(k8sNamespace); - getRenewJobNamesCmd.add("job"); - getRenewJobNamesCmd.add("-l"); - getRenewJobNamesCmd.add("canfar-net-sessionID=" + sessionID + ",canfar-net-userid=" + forUserID); - getRenewJobNamesCmd.add("--no-headers=true"); - getRenewJobNamesCmd.add("-o"); - - String customColumns = "custom-columns=" + - "NAME:.metadata.name," + - "UID:.metadata.uid," + - "STATUS:.status.active," + - "START:.status.startTime"; - - getRenewJobNamesCmd.add(customColumns); - return getRenewJobNamesCmd; - } - private void validateName(String name) { if (!StringUtil.hasText(name)) { throw new IllegalArgumentException("name must have a value"); @@ -466,10 +476,10 @@ private void validateName(String name) { } /** - * Validate and return the session type + * Validate and return the session type. There exists a loophole * * @param imageID The image to validate - * @param type User-provided session type (optional) + * @param type User-provided session type (optional), defaults to headless * @return The system recognized session type * @throws ResourceNotFoundException If an image with the supplied ID cannot be found * @throws Exception If Harbor calls fail @@ -479,39 +489,35 @@ private String validateImage(String imageID, String type) throws Exception { throw new IllegalArgumentException("image is required"); } - for (String harborHost : harborHosts) { - if (imageID.startsWith(harborHost)) { - Image image = getImage(imageID); - if (image == null) { - throw new ResourceNotFoundException("image not found or not labelled: " + imageID); - } - if (type == null) { - return image.getTypes().iterator().next(); - } else { - if (image.getTypes().contains(type)) { - return type; - } else { - throw new IllegalArgumentException("image/type mismatch: " + imageID + "/" + type); - } - } + final String imageRegistryHost = getRegistryHost(imageID); + log.debug("Image is located at " + imageRegistryHost); + + final Image image = getPublicImage(imageID); + final String validatedType; + + // Private images are also missing from this list. + if (image == null) { + log.debug("Image " + imageID + " missing from cache..."); + // Absence of type is assumed to be headless + if (SessionAction.SESSION_TYPE_HEADLESS.equals(type)) { + log.debug("Assuming headless for private (or missing) image " + imageID); + validatedType = SessionAction.SESSION_TYPE_HEADLESS; + } else { + throw new ResourceNotFoundException("image not found or not labelled: " + imageID); } + } else if (image.getTypes().contains(type)) { + validatedType = type; + } else { + throw new IllegalArgumentException("image/type mismatch: " + imageID + "/" + type); } - if (adminUser && type != null) { - if (!SESSION_TYPES.contains(type)) { + if (adminUser && validatedType != null) { + if (!SESSION_TYPES.contains(validatedType)) { throw new IllegalArgumentException("Illegal session type: " + type); } - return type; } - StringBuilder hostList = new StringBuilder("[").append(harborHosts.get(0)); - for (String next : harborHosts.subList(1, harborHosts.size())) { - hostList.append(",").append(next); - } - hostList.append("]"); - - throw new IllegalArgumentException("session image must come from one of " + hostList); - + return validatedType; } public void checkExistingSessions(String userid, String type) throws Exception { @@ -527,7 +533,7 @@ public void checkExistingSessions(String userid, String type) throws Exception { !TYPE_DESKTOP_APP.equals(session.getType())) { String status = session.getStatus(); if (!(status.equalsIgnoreCase(Session.STATUS_TERMINATING) || - status.equalsIgnoreCase(Session.STATUS_SUCCEEDED))) { + status.equalsIgnoreCase(Session.STATUS_SUCCEEDED))) { count++; } } @@ -535,13 +541,13 @@ public void checkExistingSessions(String userid, String type) throws Exception { log.debug("active interactive sessions: " + count); if (count >= maxUserSessions) { throw new IllegalArgumentException("User " + posixPrincipal.username + " has reached the maximum of " + - maxUserSessions + " active sessions."); + maxUserSessions + " active sessions."); } } public void createSession(String type, String image, String name, Integer cores, Integer ram, Integer gpus, String cmd, String args, List envs) - throws Exception { + throws Exception { String jobName = K8SUtil.getJobName(sessionID, type, posixPrincipal.username); @@ -612,18 +618,32 @@ public void createSession(String type, String image, String name, Integer cores, jobLaunchString = setConfigValue(jobLaunchString, SOFTWARE_LIMITS_RAM, ram + "Gi"); jobLaunchString = setConfigValue(jobLaunchString, SOFTWARE_LIMITS_GPUS, getGPUResourceLimit(gpus)); jobLaunchString = setConfigValue(jobLaunchString, POSIX_MAPPER_URI, posixMapperConfiguration.getBaseURL() == null - ? posixMapperConfiguration.getResourceID().toString() - : posixMapperConfiguration.getBaseURL().toExternalForm()); + ? posixMapperConfiguration.getResourceID().toString() + : posixMapperConfiguration.getBaseURL().toExternalForm()); // This property is mandatory in the Skaha configuration's cadc-registry.properties. jobLaunchString = setConfigValue(jobLaunchString, REGISTRY_URL, new LocalAuthority().getServiceURI(RegistryClient.class.getName() + ".baseURL").toString()); jobLaunchString = setConfigValue(jobLaunchString, SKAHA_TLD, skahaTld); - jobLaunchString = setConfigValue(jobLaunchString, PostAction.SOFTWARE_IMAGESECRET, createRegistryImageSecret(image)); + final String imageRegistrySecretName; + final ImageRegistryAuth userRegistryAuth; + // In the absence of the existence of a public image, assume Private. The validateImage() step above will have caught a non-existent Image already. + if (getPublicImage(image) == null) { + userRegistryAuth = getRegistryAuth(getRegistryHost(image)); + imageRegistrySecretName = createRegistryImageSecret(userRegistryAuth); + } else { + userRegistryAuth = null; + imageRegistrySecretName = PostAction.DEFAULT_SOFTWARE_IMAGESECRET_VALUE; + } + + jobLaunchString = setConfigValue(jobLaunchString, PostAction.SOFTWARE_IMAGESECRET, imageRegistrySecretName); if (type.equals(SessionAction.SESSION_TYPE_DESKTOP)) { jobLaunchString = setConfigValue(jobLaunchString, PostAction.DESKTOP_SESSION_APP_TOKEN, generateToken()); + if (userRegistryAuth != null) { + jobLaunchString = setConfigValue(jobLaunchString, PostAction.DESKTOP_SESSION_REGISTRY_AUTH, userRegistryAuth.getEncoded()); + } } String jsonLaunchFile = super.stageFile(jobLaunchString); @@ -671,16 +691,15 @@ private String generateToken() throws Exception { /** * Create a registry secret and return its name. - * @param imageID The image ID to create a secret for. - * @return String secret name, never null. + * + * @param registryAuth The credentials to use to authenticate to the Image Registry. + * @return String secret name, never null. */ - private String createRegistryImageSecret(final String imageID) { - final String registryAuth = getRegistryAuth(); + private String createRegistryImageSecret(final ImageRegistryAuth registryAuth) throws Exception { final String username = this.posixPrincipal.username; - final String secretName = "harbor-secret-" + username.toLowerCase(); - CommandExecutioner.ensureRegistrySecret(getRegistryHost(imageID), username, - StringUtil.hasText(registryAuth) ? registryAuth : PostAction.DEFAULT_SOFTWARE_IMAGESECRET_VALUE, - secretName); + final String secretName = "registry-auth-" + username.toLowerCase(); + log.debug("Creating user secret " + secretName); + CommandExecutioner.ensureRegistrySecret(registryAuth, secretName); return secretName; } @@ -688,7 +707,7 @@ private String createRegistryImageSecret(final String imageID) { private String getRegistryHost(final String imageID) { final String registryHost = this.harborHosts.stream().filter(imageID::startsWith).findFirst().orElse(null); if (registryHost == null) { - throw new IllegalArgumentException("not a skaha harbor image: " + imageID); + throw new IllegalArgumentException("session image '" + imageID + "' must come from one of " + Arrays.toString(this.harborHosts.toArray())); } return registryHost; @@ -700,6 +719,10 @@ private String getRegistryHost(final String imageID) { * TODO: and will suffer the same issue(s) with invalid characters in the Kubernetes object names. * * @param image Container image name. + * @param requestCores Requested number of cores. + * @param limitCores Max number of cores. + * @param requestRAM Requested amount of RAM in Gi. + * @param limitRAM Max amount of RAM in Gi. * @throws Exception For any unexpected errors. */ public void attachDesktopApp(String image, Integer requestCores, Integer limitCores, Integer requestRAM, @@ -709,13 +732,13 @@ public void attachDesktopApp(String image, Integer requestCores, Integer limitCo // Get the IP address based on the session String[] getIPCommand = new String[] { - "kubectl", "-n", k8sNamespace, "get", "pod", "--selector=canfar-net-sessionID=" + sessionID, - "--no-headers=true", - "-o", "custom-columns=" + - "IPADDR:.status.podIP," + - "DT:.metadata.deletionTimestamp," + - "TYPE:.metadata.labels.canfar-net-sessionType," + - "NAME:.metadata.name"}; + "kubectl", "-n", k8sNamespace, "get", "pod", "--selector=canfar-net-sessionID=" + sessionID, + "--no-headers=true", + "-o", "custom-columns=" + + "IPADDR:.status.podIP," + + "DT:.metadata.deletionTimestamp," + + "TYPE:.metadata.labels.canfar-net-sessionType," + + "NAME:.metadata.name"}; String ipResult = execute(getIPCommand); log.debug("GET IP result: " + ipResult); @@ -802,13 +825,13 @@ public void attachDesktopApp(String image, Integer requestCores, Integer limitCo // This property is mandatory in the Skaha configuration's cadc-registry.properties. launchString = setConfigValue(launchString, REGISTRY_URL, - new LocalAuthority().getServiceURI(RegistryClient.class.getName() + ".baseURL").toString()); + new LocalAuthority().getServiceURI(RegistryClient.class.getName() + ".baseURL").toString()); launchString = setConfigValue(launchString, SKAHA_TLD, skahaTld); String launchFile = super.stageFile(launchString); String[] launchCmd = new String[] { - "kubectl", "create", "--namespace", k8sNamespace, "-f", launchFile + "kubectl", "create", "--namespace", k8sNamespace, "-f", launchFile }; String createResult = execute(launchCmd); @@ -839,9 +862,9 @@ private String getSupplementalGroupsList() throws Exception { Set> groupCredentials = getCachedGroupsFromSubject(); if (groupCredentials.size() == 1) { return buildGroupUriList(groupCredentials) - .stream() - .map(posixGroup -> Integer.toString(posixGroup.getGID())) - .collect(Collectors.joining(",")); + .stream() + .map(posixGroup -> Integer.toString(posixGroup.getGID())) + .collect(Collectors.joining(",")); } else { return ""; } @@ -858,12 +881,6 @@ List toGIDs(final List groupURIS) throws Exception { return posixMapperConfiguration.getPosixMapperClient().getGID(groupURIS); } - private static Set> getCachedGroupsFromSubject() { - Subject subject = AuthenticationUtil.getCurrentSubject(); - Class> c = (Class>) (Class) List.class; - return subject.getPublicCredentials(c); - } - /** * Create the image, command, args, and env sections of the job launch yaml. Example: *

diff --git a/skaha/src/main/java/org/opencadc/skaha/session/SessionAction.java b/skaha/src/main/java/org/opencadc/skaha/session/SessionAction.java index f5c8d998..c0e8b69c 100644 --- a/skaha/src/main/java/org/opencadc/skaha/session/SessionAction.java +++ b/skaha/src/main/java/org/opencadc/skaha/session/SessionAction.java @@ -67,8 +67,6 @@ package org.opencadc.skaha.session; -import static org.opencadc.skaha.utils.CommandExecutioner.execute; - import ca.nrc.cadc.auth.AuthMethod; import ca.nrc.cadc.auth.AuthenticationUtil; import ca.nrc.cadc.auth.X509CertificateChain; @@ -234,7 +232,7 @@ private void writeClientCertificate(X509CertificateChain clientCertificateChain, // inject file String[] inject = new String[] {"mv", "-f", tmpFileName, path}; - execute(inject); + CommandExecutioner.execute(inject); } protected String getImageName(String image) { @@ -281,7 +279,7 @@ public String getPodID(String forUserID, String sessionID) throws Exception { getPodCMD.add("--no-headers=true"); getPodCMD.add("-o"); getPodCMD.add("custom-columns=NAME:.metadata.name"); - String podID = execute(getPodCMD.toArray(new String[0])); + String podID = CommandExecutioner.execute(getPodCMD.toArray(new String[0])); log.debug("podID: " + podID); if (!StringUtil.hasLength(podID)) { throw new ResourceNotFoundException("session " + sessionID + " not found."); @@ -306,7 +304,7 @@ public String getEvents(String forUserID, String sessionID) throws Exception { getEventsCmd.add("-o"); String customColumns = "TYPE:.type,REASON:.reason,MESSAGE:.message,FIRST-TIME:.firstTimestamp,LAST-TIME:.lastTimestamp"; getEventsCmd.add("custom-columns=" + customColumns); - String events = execute(getEventsCmd.toArray(new String[0])); + String events = CommandExecutioner.execute(getEventsCmd.toArray(new String[0])); log.debug("events: " + events); if (events != null) { String[] lines = events.split("\n"); @@ -328,7 +326,7 @@ public void streamPodLogs(String forUserID, String sessionID, OutputStream out) getLogsCmd.add("canfar-net-sessionID=" + sessionID + ",canfar-net-userid=" + forUserID); getLogsCmd.add("--tail"); getLogsCmd.add("-1"); - execute(getLogsCmd.toArray(new String[0]), out); + CommandExecutioner.execute(getLogsCmd.toArray(new String[0]), out); } public Session getDesktopApp(String sessionID, String appID) throws Exception { @@ -396,7 +394,7 @@ protected String toCommonUnit(String inK8sUnit) { protected Map getJobExpiryTimes(String k8sNamespace, String forUserID) throws Exception { Map jobExpiryTimes = new HashMap<>(); List jobExpiryTimeCMD = getJobExpiryTimeCMD(k8sNamespace, forUserID); - String jobExpiryTimeMap = execute(jobExpiryTimeCMD.toArray(new String[0])); + String jobExpiryTimeMap = CommandExecutioner.execute(jobExpiryTimeCMD.toArray(new String[0])); log.debug("Expiry times: " + jobExpiryTimeMap); if (StringUtil.hasLength(jobExpiryTimeMap)) { String[] lines = jobExpiryTimeMap.split("\n"); @@ -410,7 +408,7 @@ protected Map getJobExpiryTimes(String k8sNamespace, String forU } private List getJobExpiryTimeCMD(String k8sNamespace, String forUserID) { - List getSessionJobCMD = new ArrayList(); + List getSessionJobCMD = new ArrayList<>(); getSessionJobCMD.add("kubectl"); getSessionJobCMD.add("get"); getSessionJobCMD.add("--namespace"); @@ -421,9 +419,8 @@ private List getJobExpiryTimeCMD(String k8sNamespace, String forUserID) getSessionJobCMD.add("--no-headers=true"); getSessionJobCMD.add("-o"); - String customColumns = "custom-columns=" + - "UID:.metadata.uid," + - "EXPIRY:.spec.activeDeadlineSeconds"; + String customColumns; + customColumns = "custom-columns=UID:.metadata.uid,EXPIRY:.spec.activeDeadlineSeconds"; getSessionJobCMD.add(customColumns); return getSessionJobCMD; @@ -433,7 +430,7 @@ protected String getAppJobName(String sessionID, String userID, String appID) th IOException, InterruptedException { String k8sNamespace = K8SUtil.getWorkloadNamespace(); List getAppJobNameCMD = getAppJobNameCMD(k8sNamespace, userID, sessionID, appID); - return execute(getAppJobNameCMD.toArray(new String[0])); + return CommandExecutioner.execute(getAppJobNameCMD.toArray(new String[0])); } private List getAppJobNameCMD(String k8sNamespace, String userID, String sessionID, String appID) { @@ -446,7 +443,7 @@ private List getAppJobNameCMD(String k8sNamespace, String userID, String labels = labels + ",canfar-net-appID=" + appID; } - List getAppJobNameCMD = new ArrayList(); + List getAppJobNameCMD = new ArrayList<>(); getAppJobNameCMD.add("kubectl"); getAppJobNameCMD.add("get"); getAppJobNameCMD.add("--namespace"); @@ -457,8 +454,7 @@ private List getAppJobNameCMD(String k8sNamespace, String userID, String getAppJobNameCMD.add("--no-headers=true"); getAppJobNameCMD.add("-o"); - String customColumns = "custom-columns=" + - "NAME:.metadata.name"; + String customColumns = "custom-columns=NAME:.metadata.name"; getAppJobNameCMD.add(customColumns); return getAppJobNameCMD; diff --git a/skaha/src/main/java/org/opencadc/skaha/utils/CommandExecutioner.java b/skaha/src/main/java/org/opencadc/skaha/utils/CommandExecutioner.java index 42848c9f..e277e914 100644 --- a/skaha/src/main/java/org/opencadc/skaha/utils/CommandExecutioner.java +++ b/skaha/src/main/java/org/opencadc/skaha/utils/CommandExecutioner.java @@ -12,6 +12,7 @@ import java.nio.charset.StandardCharsets; import java.util.Arrays; import org.opencadc.skaha.K8SUtil; +import org.opencadc.skaha.registry.ImageRegistryAuth; public class CommandExecutioner { private static final Logger log = Logger.getLogger(CommandExecutioner.class); @@ -81,15 +82,52 @@ public static void execute(final String[] command, final OutputStream standardOu } } - public static void ensureRegistrySecret(final String registryHost, final String registryUsername, final String secret, final String secretName) { - // create new secret - String[] createCmd = new String[] { - "kubectl", "--namespace", K8SUtil.getWorkloadNamespace(), "create", "secret", "docker-registry", - secretName, - "--docker-server=" + registryHost, - "--docker-username=" + registryUsername, - "--docker-password=" + secret - }; + public static void ensureRegistrySecret(final ImageRegistryAuth registryAuth, final String secretName) + throws Exception { + // delete any old secret by this name + final String[] deleteCmd = new String[] {"kubectl", "--namespace", K8SUtil.getWorkloadNamespace(), "delete", "secret", secretName}; + log.debug("delete secret command: " + Arrays.toString(deleteCmd)); + try { + String deleteResult = CommandExecutioner.execute(deleteCmd); + log.debug("delete secret result: " + deleteResult); + } catch (IOException notFound) { + log.debug("no secret to delete", notFound); + } + + // harbor invalidates secrets with the Unicode replacement characters 'fffd'. + if (registryAuth.isSecretValid()) { + // create new secret + final String[] createCmd = new String[] { + "kubectl", "--namespace", K8SUtil.getWorkloadNamespace(), "create", "secret", "docker-registry", + secretName, + "--docker-server=" + registryAuth.getHost(), + "--docker-username=" + registryAuth.getUsername(), + "--docker-password=" + new String(registryAuth.getSecret()) + }; + log.debug("create secret command: " + Arrays.toString(createCmd)); + + try { + String createResult = CommandExecutioner.execute(createCmd); + log.debug("create secret result: " + createResult); + } catch (IOException e) { + if (e.getMessage() != null && e.getMessage().toLowerCase().contains("already exists")) { + // This can happen with concurrent posts by same user. + // Considered making secrets unique with the session id, + // but that would lead to a large number of secrets and there + // is no k8s option to have them cleaned up automatically. + // Should look at supporting multiple job creations on a post, + // specifically for the headless use case. That way only one + // secret per post. + log.warn("secret creation failed, moving on: " + e); + } else { + log.error(e.getMessage(), e); + throw new IOException("error creating image pull secret"); + } + } + } else { + log.warn("image repository 'CLI Secret' is invalid and needs resetting."); + /* @TODO: Should throw IllegalStateException here, but for future work. */ + } } public static JSONObject getSecretData(final String secretName, final String secretNamespace) throws Exception { diff --git a/skaha/src/main/java/org/opencadc/skaha/utils/PosixCache.java b/skaha/src/main/java/org/opencadc/skaha/utils/PosixCache.java index f19e8257..3ae3fbab 100644 --- a/skaha/src/main/java/org/opencadc/skaha/utils/PosixCache.java +++ b/skaha/src/main/java/org/opencadc/skaha/utils/PosixCache.java @@ -32,6 +32,7 @@ public class PosixCache { * Construct a new Cache. This will initialize the Redis Pool (JediPooled) with the given URL and client to the POSIX Mapper API. * * @param cacheURL The Redis URL. + * @param rootHomeFolder Root of entire system (i.e. containing home and project folders) * @param posixMapperClient The Client to the POSIX Mapper API. */ public PosixCache(final String cacheURL, final String rootHomeFolder, final PosixMapperClient posixMapperClient) { From a84525f841a5dfdda0f567aa70a0e3147925cfb8 Mon Sep 17 00:00:00 2001 From: Dustin Jenkins Date: Tue, 15 Oct 2024 18:27:23 +0000 Subject: [PATCH 04/15] fix: review rework --- .../helm/skaha/desktop-template/start-software-sh.template | 4 ++-- .../src/main/java/org/opencadc/skaha/session/PostAction.java | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/deployment/helm/skaha/desktop-template/start-software-sh.template b/deployment/helm/skaha/desktop-template/start-software-sh.template index 28e79947..cc2360e4 100644 --- a/deployment/helm/skaha/desktop-template/start-software-sh.template +++ b/deployment/helm/skaha/desktop-template/start-software-sh.template @@ -125,7 +125,7 @@ get_status() { if [ -z "${TOKEN}" ]; then handle_error "[skaha] No credentials to call back to Skaha with." else - curl_out=`curl -s -L -k "${CURL_APP_SESSION_HEADERS}" https://(HOST)/skaha/(SKAHA_API_VERSION)/session/${VNC_PW}/app/$1` + curl_out=`curl -s -L -k --header "x-auth-token-skaha: ${TOKEN}" https://(HOST)/skaha/(SKAHA_API_VERSION)/session/${VNC_PW}/app/$1` fi while [[ ${curl_out} != *"status"* ]]; do @@ -137,7 +137,7 @@ get_status() { sleep 1 # No need to check for the Token again as it passed above. - curl_out=`curl -s -L -k "${CURL_APP_SESSION_HEADERS}" https://(HOST)/skaha/(SKAHA_API_VERSION)/session/${VNC_PW}/app/$1` + curl_out=`curl -s -L -k --header "x-auth-token-skaha: ${TOKEN}" https://(HOST)/skaha/(SKAHA_API_VERSION)/session/${VNC_PW}/app/$1` done } diff --git a/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java b/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java index 7ac65c75..458c7bf4 100644 --- a/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java +++ b/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java @@ -498,7 +498,6 @@ private String validateImage(String imageID, String type) throws Exception { // Private images are also missing from this list. if (image == null) { log.debug("Image " + imageID + " missing from cache..."); - // Absence of type is assumed to be headless if (SessionAction.SESSION_TYPE_HEADLESS.equals(type)) { log.debug("Assuming headless for private (or missing) image " + imageID); validatedType = SessionAction.SESSION_TYPE_HEADLESS; From d89c75896f2b3193298bc576b43b1b14e3ebf574 Mon Sep 17 00:00:00 2001 From: Dustin Jenkins Date: Tue, 15 Oct 2024 22:13:01 +0000 Subject: [PATCH 05/15] fix: remove unnecessary check --- .../skaha/registry/ImageRegistryAuth.java | 4 -- .../opencadc/skaha/session/SessionAction.java | 5 +- .../skaha/utils/CommandExecutioner.java | 58 +++++++++---------- 3 files changed, 29 insertions(+), 38 deletions(-) diff --git a/skaha/src/main/java/org/opencadc/skaha/registry/ImageRegistryAuth.java b/skaha/src/main/java/org/opencadc/skaha/registry/ImageRegistryAuth.java index b7dac013..f00dc132 100644 --- a/skaha/src/main/java/org/opencadc/skaha/registry/ImageRegistryAuth.java +++ b/skaha/src/main/java/org/opencadc/skaha/registry/ImageRegistryAuth.java @@ -65,8 +65,4 @@ public String getUsername() { public byte[] getSecret() { return secret; } - - public boolean isSecretValid() { - return !new String(this.secret).startsWith("\ufffd"); - } } diff --git a/skaha/src/main/java/org/opencadc/skaha/session/SessionAction.java b/skaha/src/main/java/org/opencadc/skaha/session/SessionAction.java index c0e8b69c..ff852d8b 100644 --- a/skaha/src/main/java/org/opencadc/skaha/session/SessionAction.java +++ b/skaha/src/main/java/org/opencadc/skaha/session/SessionAction.java @@ -329,13 +329,14 @@ public void streamPodLogs(String forUserID, String sessionID, OutputStream out) CommandExecutioner.execute(getLogsCmd.toArray(new String[0]), out); } + @SuppressWarnings("checkstyle:OperatorWrap") public Session getDesktopApp(String sessionID, String appID) throws Exception { List sessions = SessionDAO.getSessions(posixPrincipal.username, sessionID, skahaTld); if (!sessions.isEmpty()) { for (Session session : sessions) { // only include 'desktop-app' - if (SkahaAction.TYPE_DESKTOP_APP.equalsIgnoreCase(session.getType()) && - (sessionID.equals(session.getId())) && (appID.equals(session.getAppId()))) { + if (SkahaAction.TYPE_DESKTOP_APP.equalsIgnoreCase(session.getType()) + && (sessionID.equals(session.getId())) && (appID.equals(session.getAppId()))) { return session; } } diff --git a/skaha/src/main/java/org/opencadc/skaha/utils/CommandExecutioner.java b/skaha/src/main/java/org/opencadc/skaha/utils/CommandExecutioner.java index e277e914..44ebad0e 100644 --- a/skaha/src/main/java/org/opencadc/skaha/utils/CommandExecutioner.java +++ b/skaha/src/main/java/org/opencadc/skaha/utils/CommandExecutioner.java @@ -94,39 +94,33 @@ public static void ensureRegistrySecret(final ImageRegistryAuth registryAuth, fi log.debug("no secret to delete", notFound); } - // harbor invalidates secrets with the Unicode replacement characters 'fffd'. - if (registryAuth.isSecretValid()) { - // create new secret - final String[] createCmd = new String[] { - "kubectl", "--namespace", K8SUtil.getWorkloadNamespace(), "create", "secret", "docker-registry", - secretName, - "--docker-server=" + registryAuth.getHost(), - "--docker-username=" + registryAuth.getUsername(), - "--docker-password=" + new String(registryAuth.getSecret()) - }; - log.debug("create secret command: " + Arrays.toString(createCmd)); - - try { - String createResult = CommandExecutioner.execute(createCmd); - log.debug("create secret result: " + createResult); - } catch (IOException e) { - if (e.getMessage() != null && e.getMessage().toLowerCase().contains("already exists")) { - // This can happen with concurrent posts by same user. - // Considered making secrets unique with the session id, - // but that would lead to a large number of secrets and there - // is no k8s option to have them cleaned up automatically. - // Should look at supporting multiple job creations on a post, - // specifically for the headless use case. That way only one - // secret per post. - log.warn("secret creation failed, moving on: " + e); - } else { - log.error(e.getMessage(), e); - throw new IOException("error creating image pull secret"); - } + // create new secret + final String[] createCmd = new String[] { + "kubectl", "--namespace", K8SUtil.getWorkloadNamespace(), "create", "secret", "docker-registry", + secretName, + "--docker-server=" + registryAuth.getHost(), + "--docker-username=" + registryAuth.getUsername(), + "--docker-password=" + new String(registryAuth.getSecret()) + }; + log.debug("create secret command: " + Arrays.toString(createCmd)); + + try { + String createResult = CommandExecutioner.execute(createCmd); + log.debug("create secret result: " + createResult); + } catch (IOException e) { + if (e.getMessage() != null && e.getMessage().toLowerCase().contains("already exists")) { + // This can happen with concurrent posts by same user. + // Considered making secrets unique with the session id, + // but that would lead to a large number of secrets and there + // is no k8s option to have them cleaned up automatically. + // Should look at supporting multiple job creations on a post, + // specifically for the headless use case. That way only one + // secret per post. + log.warn("secret creation failed, moving on: " + e); + } else { + log.error(e.getMessage(), e); + throw new IOException("error creating image pull secret"); } - } else { - log.warn("image repository 'CLI Secret' is invalid and needs resetting."); - /* @TODO: Should throw IllegalStateException here, but for future work. */ } } From aacc3c00fd01d5e4143afe3b7d1b6e37c275a08e Mon Sep 17 00:00:00 2001 From: Dustin Jenkins Date: Tue, 15 Oct 2024 23:30:58 +0000 Subject: [PATCH 06/15] fix: allow different types of private images to accommodate the ui --- .../org/opencadc/skaha/session/PostAction.java | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java b/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java index 458c7bf4..1fbe6a08 100644 --- a/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java +++ b/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java @@ -489,6 +489,8 @@ private String validateImage(String imageID, String type) throws Exception { throw new IllegalArgumentException("image is required"); } + // This will also vet the currently requested image's host (authority) against + // the list of configured ones. final String imageRegistryHost = getRegistryHost(imageID); log.debug("Image is located at " + imageRegistryHost); @@ -496,13 +498,19 @@ private String validateImage(String imageID, String type) throws Exception { final String validatedType; // Private images are also missing from this list. + // TODO: We currently rely on the image's host name to match a configured one + // TODO: to ensure a supported image from a configured source. This is impossible + // TODO: with Private images as they cannot be obtained first. This means that any + // TODO: image that is missing from the Public Cache can either be invalid or Private, + // TODO: and since we can't verify one way or the other, let them through. if (image == null) { - log.debug("Image " + imageID + " missing from cache..."); - if (SessionAction.SESSION_TYPE_HEADLESS.equals(type)) { - log.debug("Assuming headless for private (or missing) image " + imageID); - validatedType = SessionAction.SESSION_TYPE_HEADLESS; - } else { + log.warn("Image " + imageID + " missing from cache..."); + final ImageRegistryAuth imageRegistryAuth = getRegistryAuth(imageRegistryHost); + if (imageRegistryAuth == null) { throw new ResourceNotFoundException("image not found or not labelled: " + imageID); + } else { + log.warn("Assuming image " + imageID + " is private as credentials were supplied."); + validatedType = type; } } else if (image.getTypes().contains(type)) { validatedType = type; From db458de8ca9414053e34cb7093e390c07758578d Mon Sep 17 00:00:00 2001 From: Dustin Jenkins Date: Wed, 16 Oct 2024 22:21:17 +0000 Subject: [PATCH 07/15] fix: update version for science portal chart --- deployment/helm/science-portal/Chart.yaml | 4 ++-- deployment/helm/science-portal/values.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/deployment/helm/science-portal/Chart.yaml b/deployment/helm/science-portal/Chart.yaml index 7e33c334..68f01a23 100644 --- a/deployment/helm/science-portal/Chart.yaml +++ b/deployment/helm/science-portal/Chart.yaml @@ -15,13 +15,13 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.2.11 +version: 0.3.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "0.2.8" +appVersion: "0.3.0" dependencies: - name: "redis" diff --git a/deployment/helm/science-portal/values.yaml b/deployment/helm/science-portal/values.yaml index 7f2592f5..9d60f718 100644 --- a/deployment/helm/science-portal/values.yaml +++ b/deployment/helm/science-portal/values.yaml @@ -11,7 +11,7 @@ skaha: deployment: hostname: example.host.com # Change this! sciencePortal: - image: images.opencadc.org/platform/science-portal:0.2.8 + image: images.opencadc.org/platform/science-portal:0.3.0 imagePullPolicy: Always # Optionally set the DEBUG port. From 55fd55b15c6c2b55dc2c5ed0f6e4e1c557597af7 Mon Sep 17 00:00:00 2001 From: Dustin Jenkins Date: Mon, 21 Oct 2024 20:09:04 +0000 Subject: [PATCH 08/15] style: lint fixing --- skaha/src/main/java/org/opencadc/skaha/SkahaAction.java | 9 ++++----- .../main/java/org/opencadc/skaha/session/PostAction.java | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/skaha/src/main/java/org/opencadc/skaha/SkahaAction.java b/skaha/src/main/java/org/opencadc/skaha/SkahaAction.java index ec74e5f7..8cd07f34 100644 --- a/skaha/src/main/java/org/opencadc/skaha/SkahaAction.java +++ b/skaha/src/main/java/org/opencadc/skaha/SkahaAction.java @@ -234,10 +234,8 @@ private static EncodedKeyPair getPreAuthorizedTokenSecret() throws Exception { final String[] createCmd = new String[] { "kubectl", "--namespace", K8SUtil.getWorkloadNamespace(), "create", "secret", "generic", K8SUtil.getPreAuthorizedTokenSecretName(), - String.format("--from-literal=%s=", publicKeyPropertyName) - + CommonUtils.encodeBase64(encodedPublicKey), - String.format("--from-literal=%s=", privateKeyPropertyName) - + CommonUtils.encodeBase64(encodedPrivateKey) + String.format("--from-literal=%s=%s", publicKeyPropertyName, CommonUtils.encodeBase64(encodedPublicKey)), + String.format("--from-literal=%s=%s", privateKeyPropertyName, CommonUtils.encodeBase64(encodedPrivateKey)) }; final String createResult = CommandExecutioner.execute(createCmd); @@ -317,7 +315,6 @@ private void initiateSkahaCallbackFlow(Subject currentSubject, URI skahaUsersUri private void initiateGeneralFlow(Subject currentSubject, URI skahaUsersUri) throws IOException, InterruptedException, ResourceNotFoundException { - GroupURI skahaUsersGroupUri = new GroupURI(skahaUsersUri); if (currentSubject == null || currentSubject.getPrincipals().isEmpty()) { throw new NotAuthenticatedException("Unauthorized"); } @@ -343,6 +340,7 @@ private void initiateGeneralFlow(Subject currentSubject, URI skahaUsersUri) log.debug("userID: " + posixPrincipal + " (" + posixPrincipal.username + ")"); // ensure user is a part of the skaha group + LocalAuthority localAuthority = new LocalAuthority(); URI gmsSearchURI = localAuthority.getServiceURI(Standards.GMS_SEARCH_10.toString()); @@ -365,6 +363,7 @@ private void initiateGeneralFlow(Subject currentSubject, URI skahaUsersUri) priorityHeadlessUser = true; } + final GroupURI skahaUsersGroupUri = new GroupURI(skahaUsersUri); if (!skahaUsersGroupUriSet.contains(skahaUsersGroupUri)) { log.debug("user is not a member of skaha user group "); throw new AccessControlException("Not authorized to use the skaha system"); diff --git a/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java b/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java index 1fbe6a08..75b63efc 100644 --- a/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java +++ b/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java @@ -890,7 +890,7 @@ List toGIDs(final List groupURIS) throws Exception { /** * Create the image, command, args, and env sections of the job launch yaml. Example: - *

+ *

* image: "${software.imageid}" * command: ["/skaha-system/start-desktop-software.sh"] * args: [arg1, arg2] From 1162efb9fd6fbb65e5a8d510997f0286df7ab2d2 Mon Sep 17 00:00:00 2001 From: Dustin Jenkins Date: Wed, 23 Oct 2024 18:22:06 +0000 Subject: [PATCH 09/15] fix: remove registry auth information for desktop app launching --- .../desktop-template/start-software-sh.template | 3 +-- .../helm/skaha/skaha-config/launch-desktop.yaml | 2 -- .../org/opencadc/skaha/session/PostAction.java | 15 ++++----------- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/deployment/helm/skaha/desktop-template/start-software-sh.template b/deployment/helm/skaha/desktop-template/start-software-sh.template index cc2360e4..259735cf 100644 --- a/deployment/helm/skaha/desktop-template/start-software-sh.template +++ b/deployment/helm/skaha/desktop-template/start-software-sh.template @@ -3,8 +3,7 @@ HOST=(HOST) # Callback token TOKEN="${DESKTOP_SESSION_APP_TOKEN}" -REGISTRY_AUTH="${DESKTOP_SESSION_APP_REGISTRY_AUTH}" -CURL_APP_SESSION_HEADERS="--header \"x-auth-token-skaha: ${TOKEN}\" --header \"x-skaha-registry-auth: ${REGISTRY_AUTH}\"" +CURL_APP_SESSION_HEADERS="--header \"x-auth-token-skaha: ${TOKEN}\"" handle_error() { echo "$1" diff --git a/deployment/helm/skaha/skaha-config/launch-desktop.yaml b/deployment/helm/skaha/skaha-config/launch-desktop.yaml index 614b455e..7fa1af74 100644 --- a/deployment/helm/skaha/skaha-config/launch-desktop.yaml +++ b/deployment/helm/skaha/skaha-config/launch-desktop.yaml @@ -57,8 +57,6 @@ spec: value: "v0" - name: DESKTOP_SESSION_APP_TOKEN value: "${software.desktop.app.token}" - - name: DESKTOP_SESSION_APP_REGISTRY_AUTH - value: "${software.desktop.app.registry-auth}" securityContext: privileged: false allowPrivilegeEscalation: false diff --git a/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java b/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java index e70df3b8..8bda5da2 100644 --- a/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java +++ b/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java @@ -136,13 +136,12 @@ public class PostAction extends SessionAction { public static final String HEADLESS_PRIORITY = "headless.priority"; public static final String HEADLESS_IMAGE_BUNDLE = "headless.image.bundle"; public static final String DEFAULT_SOFTWARE_IMAGESECRET_VALUE = "notused"; + // k8s rejects label size > 63. Since k8s appends a maximum of six characters // to a job name to form a pod name, we limit the job name length to 57 characters. private static final int MAX_JOB_NAME_LENGTH = 57; private static final String CREATE_USER_BASE_COMMAND = "/usr/local/bin/add-user"; private static final String DESKTOP_SESSION_APP_TOKEN = "software.desktop.app.token"; - private static final String DESKTOP_SESSION_REGISTRY_AUTH = "software.desktop.app.registry-auth"; - private static final String POSIX_MAPPER_URI = "POSIX_MAPPER_URI"; private static final String SKAHA_TLD = "SKAHA_TLD"; private static final Logger log = Logger.getLogger(PostAction.class); @@ -547,8 +546,6 @@ public void createSession(String type, String image, String name, Integer cores, String cmd, String args, List envs) throws Exception { - String jobName = K8SUtil.getJobName(sessionID, type, posixPrincipal.username); - String supplementalGroups = getSupplementalGroupsList(); log.debug("supplementalGroups are " + supplementalGroups); @@ -590,16 +587,16 @@ public void createSession(String type, String image, String name, Integer cores, final String headlessImageBundle = getHeadlessImageBundle(image, cmd, args, envs); final String imageRegistrySecretName; - final ImageRegistryAuth userRegistryAuth; // In the absence of the existence of a public image, assume Private. The validateImage() step above will have caught a non-existent Image already. if (getPublicImage(image) == null) { - userRegistryAuth = getRegistryAuth(getRegistryHost(image)); + final ImageRegistryAuth userRegistryAuth = getRegistryAuth(getRegistryHost(image)); imageRegistrySecretName = createRegistryImageSecret(userRegistryAuth); } else { - userRegistryAuth = null; imageRegistrySecretName = PostAction.DEFAULT_SOFTWARE_IMAGESECRET_VALUE; } + String jobName = K8SUtil.getJobName(sessionID, type, posixPrincipal.username); + SessionJobBuilder sessionJobBuilder = SessionJobBuilder.fromPath(Paths.get(jobLaunchPath)) .withGPUEnabled(this.gpuEnabled) .withGPUCount(gpus) @@ -620,10 +617,6 @@ public void createSession(String type, String image, String name, Integer cores, .withParameter(PostAction.SOFTWARE_REQUESTS_RAM, ram.toString() + "Gi") .withParameter(PostAction.SOFTWARE_LIMITS_CORES, cores.toString()) .withParameter(PostAction.SOFTWARE_LIMITS_RAM, ram + "Gi") - .withParameter(PostAction.POSIX_MAPPER_URI, - this.posixMapperConfiguration.getBaseURL() == null - ? this.posixMapperConfiguration.getResourceID().toString() - : this.posixMapperConfiguration.getBaseURL().toExternalForm()) .withParameter(PostAction.SKAHA_TLD, this.skahaTld); if (StringUtil.hasText(supplementalGroups)) { From 60e524be0e7202fe7de6082efdbe2ea9a87882ec Mon Sep 17 00:00:00 2001 From: Dustin Jenkins Date: Wed, 23 Oct 2024 18:27:41 +0000 Subject: [PATCH 10/15] fix: code review cleanup --- .../java/org/opencadc/skaha/registry/ImageRegistryAuth.java | 3 ++- .../main/java/org/opencadc/skaha/session/SessionAction.java | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/skaha/src/main/java/org/opencadc/skaha/registry/ImageRegistryAuth.java b/skaha/src/main/java/org/opencadc/skaha/registry/ImageRegistryAuth.java index f00dc132..4b27ba08 100644 --- a/skaha/src/main/java/org/opencadc/skaha/registry/ImageRegistryAuth.java +++ b/skaha/src/main/java/org/opencadc/skaha/registry/ImageRegistryAuth.java @@ -2,6 +2,7 @@ import ca.nrc.cadc.util.Base64; import ca.nrc.cadc.util.StringUtil; +import java.nio.charset.StandardCharsets; /** @@ -40,7 +41,7 @@ public static ImageRegistryAuth fromEncoded(final String encodedValue, final Str throw new IllegalArgumentException("Encoded auth username and key is required."); } - final String decodedValue = new String(Base64.decode(encodedValue)); + final String decodedValue = new String(Base64.decode(encodedValue), StandardCharsets.UTF_8); final String[] values = decodedValue.split(ImageRegistryAuth.VALUE_DELIMITER); if (values.length != 2) { diff --git a/skaha/src/main/java/org/opencadc/skaha/session/SessionAction.java b/skaha/src/main/java/org/opencadc/skaha/session/SessionAction.java index 6f20dd64..b357e8d5 100644 --- a/skaha/src/main/java/org/opencadc/skaha/session/SessionAction.java +++ b/skaha/src/main/java/org/opencadc/skaha/session/SessionAction.java @@ -328,7 +328,6 @@ public void streamPodLogs(String forUserID, String sessionID, OutputStream out) CommandExecutioner.execute(getLogsCmd.toArray(new String[0]), out); } - @SuppressWarnings("checkstyle:OperatorWrap") public Session getDesktopApp(String sessionID, String appID) throws Exception { List sessions = SessionDAO.getSessions(posixPrincipal.username, sessionID, skahaTld); if (!sessions.isEmpty()) { From 792f1e4fa829d0b01df85ba8b31a589939194731 Mon Sep 17 00:00:00 2001 From: Dustin Jenkins Date: Thu, 31 Oct 2024 19:03:53 +0000 Subject: [PATCH 11/15] docs: documentation and readme updates to configure tab panel labels --- deployment/helm/science-portal/README.md | 10 ++++++++++ .../config/org.opencadc.science-portal.properties | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/deployment/helm/science-portal/README.md b/deployment/helm/science-portal/README.md index fa0f4bb7..703ff834 100644 --- a/deployment/helm/science-portal/README.md +++ b/deployment/helm/science-portal/README.md @@ -57,6 +57,16 @@ deployment: # The Resource ID of the Service that contains the URL of the Skaha service in the IVOA Registry skahaResourceID: ivo://example.org/skaha + # Array of tab labels from left to right. There are two supported tabs currently: Public (Standard) and Private (Advanced) + # Recommended is Standard and Advanced, but you do you. + # Example: + # + # tabLabels: + # - Standard + # - Advanced + # + tabLabels: [] + # The logo in the top left. No link associated, just the image. This can be relative, or absolute. # Default is the SRCNet Logo. # logoURL: /science-portal/images/SRCNetLogo.png diff --git a/deployment/helm/science-portal/config/org.opencadc.science-portal.properties b/deployment/helm/science-portal/config/org.opencadc.science-portal.properties index 613f2908..b100af44 100644 --- a/deployment/helm/science-portal/config/org.opencadc.science-portal.properties +++ b/deployment/helm/science-portal/config/org.opencadc.science-portal.properties @@ -3,6 +3,12 @@ org.opencadc.science-portal.sessions.standard = vos://cadc.nrc.ca~vospace/CADC/s org.opencadc.science-portal.logoURL = {{ .Values.deployment.sciencePortal.logoURL }} org.opencadc.science-portal.themeName = {{ .Values.deployment.sciencePortal.themeName | default "src" }} +{{- if empty .Values.deployment.sciencePortal.tabLabels }} + {{ required ".Values.deployment.sciencePortal.tabLabels is missing or empty" .Values.deployment.sciencePortal.tabLabels }} +{{- else }} + org.opencadc.science-portal.tabLabels = {{ .Values.deployment.sciencePortal.tabLabels | join "," }} +{{- end }} + {{- with .Values.deployment.sciencePortal.oidc }} org.opencadc.science-portal.oidc.clientID = {{ .clientID }} From 123b9bb1eb330ce83b59fd67bee749b2a51753ad Mon Sep 17 00:00:00 2001 From: Dustin Jenkins Date: Wed, 6 Nov 2024 21:16:48 +0000 Subject: [PATCH 12/15] fix: properly expand variable for redis url --- .../skaha/init-users-groups-config/init-users-groups.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/deployment/helm/skaha/init-users-groups-config/init-users-groups.sh b/deployment/helm/skaha/init-users-groups-config/init-users-groups.sh index 1483a82e..f16a301f 100755 --- a/deployment/helm/skaha/init-users-groups-config/init-users-groups.sh +++ b/deployment/helm/skaha/init-users-groups-config/init-users-groups.sh @@ -15,7 +15,9 @@ IFS='\n' if [[ -z "${REDIS_URL}" ]]; then echo "Required argument REDIS_URL is missing." exit 1 -fi +else + echo "Using REDIS_URL: ${REDIS_URL}" +fi TARGET_PASSWD_FILE="/etc-passwd/passwd" TARGET_GROUP_FILE="/etc-group/group" @@ -25,8 +27,8 @@ cat /etc-passwd/passwd-orig > "${TARGET_PASSWD_FILE}" cat /etc-group/group-orig > "${TARGET_GROUP_FILE}" # Append Science Platform users -redis-cli -u ${REDIS_URL} --raw smembers "users:posix" >> "${TARGET_PASSWD_FILE}" -redis-cli -u ${REDIS_URL} --raw smembers "groups:posix" >> "${TARGET_GROUP_FILE}" +redis-cli -u "${REDIS_URL}" --raw smembers "users:posix" >> "${TARGET_PASSWD_FILE}" +redis-cli -u "${REDIS_URL}" --raw smembers "groups:posix" >> "${TARGET_GROUP_FILE}" # restore $IFS IFS=$SAVEIFS From 6027f29ef4ccd12e2426bae5dd06d303f62e6d9e Mon Sep 17 00:00:00 2001 From: Dustin Jenkins Date: Thu, 7 Nov 2024 17:30:40 +0000 Subject: [PATCH 13/15] fix: increase test coverage and small checks and cleanup --- .../skaha/registry/ImageRegistryAuth.java | 2 + .../skaha/utils/CommandExecutioner.java | 56 ++++++++++++------- .../skaha/registry/ImageRegistryAuthTest.java | 42 ++++++++++++++ .../skaha/session/SessionJobBuilderTest.java | 15 +++-- .../resources/test-base-values-affinity.yaml | 2 + 5 files changed, 93 insertions(+), 24 deletions(-) create mode 100644 skaha/src/test/java/org/opencadc/skaha/registry/ImageRegistryAuthTest.java diff --git a/skaha/src/main/java/org/opencadc/skaha/registry/ImageRegistryAuth.java b/skaha/src/main/java/org/opencadc/skaha/registry/ImageRegistryAuth.java index 4b27ba08..50bfa4fd 100644 --- a/skaha/src/main/java/org/opencadc/skaha/registry/ImageRegistryAuth.java +++ b/skaha/src/main/java/org/opencadc/skaha/registry/ImageRegistryAuth.java @@ -39,6 +39,8 @@ public class ImageRegistryAuth { public static ImageRegistryAuth fromEncoded(final String encodedValue, final String host) { if (!StringUtil.hasText(encodedValue)) { throw new IllegalArgumentException("Encoded auth username and key is required."); + } else if (!StringUtil.hasText(host)) { + throw new IllegalArgumentException("Registry host is required."); } final String decodedValue = new String(Base64.decode(encodedValue), StandardCharsets.UTF_8); diff --git a/skaha/src/main/java/org/opencadc/skaha/utils/CommandExecutioner.java b/skaha/src/main/java/org/opencadc/skaha/utils/CommandExecutioner.java index 44ebad0e..46e1d86e 100644 --- a/skaha/src/main/java/org/opencadc/skaha/utils/CommandExecutioner.java +++ b/skaha/src/main/java/org/opencadc/skaha/utils/CommandExecutioner.java @@ -1,16 +1,19 @@ package org.opencadc.skaha.utils; import ca.nrc.cadc.util.StringUtil; -import org.apache.log4j.Logger; -import org.json.JSONObject; - -import java.io.*; +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import org.apache.log4j.Logger; +import org.json.JSONObject; import org.opencadc.skaha.K8SUtil; import org.opencadc.skaha.registry.ImageRegistryAuth; @@ -61,7 +64,7 @@ public static void execute(String[] command, OutputStream out) throws IOExceptio } public static void execute(final String[] command, final OutputStream standardOut, final OutputStream standardErr) - throws IOException, InterruptedException { + throws IOException, InterruptedException { final Process p = Runtime.getRuntime().exec(command); final int code = p.waitFor(); try (final InputStream stdOut = new BufferedInputStream(p.getInputStream()); @@ -82,10 +85,17 @@ public static void execute(final String[] command, final OutputStream standardOu } } + /** + * Delete, if necessary, and recreate the image pull secret for the given registry. + * + * @param registryAuth The registry credentials. + * @param secretName The name of the secret to create. + * @throws Exception If there is an error creating the secret. + */ public static void ensureRegistrySecret(final ImageRegistryAuth registryAuth, final String secretName) throws Exception { // delete any old secret by this name - final String[] deleteCmd = new String[] {"kubectl", "--namespace", K8SUtil.getWorkloadNamespace(), "delete", "secret", secretName}; + final String[] deleteCmd = CommandExecutioner.getDeleteSecretCommand(secretName); log.debug("delete secret command: " + Arrays.toString(deleteCmd)); try { String deleteResult = CommandExecutioner.execute(deleteCmd); @@ -95,13 +105,7 @@ public static void ensureRegistrySecret(final ImageRegistryAuth registryAuth, fi } // create new secret - final String[] createCmd = new String[] { - "kubectl", "--namespace", K8SUtil.getWorkloadNamespace(), "create", "secret", "docker-registry", - secretName, - "--docker-server=" + registryAuth.getHost(), - "--docker-username=" + registryAuth.getUsername(), - "--docker-password=" + new String(registryAuth.getSecret()) - }; + final String[] createCmd = CommandExecutioner.getRegistryCreateSecretCommand(registryAuth, secretName); log.debug("create secret command: " + Arrays.toString(createCmd)); try { @@ -124,11 +128,25 @@ public static void ensureRegistrySecret(final ImageRegistryAuth registryAuth, fi } } + static String[] getDeleteSecretCommand(final String secretName) { + return new String[] {"kubectl", "--namespace", K8SUtil.getWorkloadNamespace(), "delete", "secret", secretName}; + } + + static String[] getRegistryCreateSecretCommand(final ImageRegistryAuth registryAuth, final String secretName) { + return new String[] { + "kubectl", "--namespace", K8SUtil.getWorkloadNamespace(), "create", "secret", "docker-registry", + secretName, + "--docker-server=" + registryAuth.getHost(), + "--docker-username=" + registryAuth.getUsername(), + "--docker-password=" + new String(registryAuth.getSecret()) + }; + } + public static JSONObject getSecretData(final String secretName, final String secretNamespace) throws Exception { // Check the current secret final String[] getSecretCommand = new String[] { - "kubectl", "--namespace", secretNamespace, "get", "--ignore-not-found", "secret", - secretName, "-o", "jsonpath=\"{.data}\"" + "kubectl", "--namespace", secretNamespace, "get", "--ignore-not-found", "secret", + secretName, "-o", "jsonpath=\"{.data}\"" }; final String data = CommandExecutioner.execute(getSecretCommand); @@ -136,13 +154,13 @@ public static JSONObject getSecretData(final String secretName, final String sec // The data from the output begins with a double-quote and ends with one, so strip them. return StringUtil.hasText(data) ? new JSONObject(data.replaceFirst("\"", "") .substring(0, data.lastIndexOf("\""))) - : new JSONObject(); + : new JSONObject(); } protected static String readStream(InputStream in) throws IOException { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - int nRead; byte[] data = new byte[1024]; + int nRead; while ((nRead = in.read(data, 0, data.length)) != -1) { buffer.write(data, 0, nRead); } @@ -150,7 +168,7 @@ protected static String readStream(InputStream in) throws IOException { } public static void changeOwnership(String path, int posixId, int groupId) throws IOException, InterruptedException { - String[] chown = new String[]{"chown", posixId + ":" + groupId, path}; - execute(chown); + String[] chown = new String[] {"chown", posixId + ":" + groupId, path}; + CommandExecutioner.execute(chown); } } diff --git a/skaha/src/test/java/org/opencadc/skaha/registry/ImageRegistryAuthTest.java b/skaha/src/test/java/org/opencadc/skaha/registry/ImageRegistryAuthTest.java new file mode 100644 index 00000000..94ef3cda --- /dev/null +++ b/skaha/src/test/java/org/opencadc/skaha/registry/ImageRegistryAuthTest.java @@ -0,0 +1,42 @@ +package org.opencadc.skaha.registry; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import org.junit.Assert; +import org.junit.Test; + +public class ImageRegistryAuthTest { + @Test + public void testFromEncodedBadInputs() { + try { + ImageRegistryAuth.fromEncoded(null, "host"); + Assert.fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + Assert.assertEquals("Encoded auth username and key is required.", e.getMessage()); + } + + try { + ImageRegistryAuth.fromEncoded("", "host"); + Assert.fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + Assert.assertEquals("Encoded auth username and key is required.", e.getMessage()); + } + + try { + ImageRegistryAuth.fromEncoded("value", null); + Assert.fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + Assert.assertEquals("Registry host is required.", e.getMessage()); + } + } + + @Test + public void testFromEncoded() { + final String encodedValue = new String(Base64.getEncoder().encode("username:supersecret".getBytes(StandardCharsets.UTF_8))); + final ImageRegistryAuth auth = ImageRegistryAuth.fromEncoded(encodedValue, "host.example.com"); + + Assert.assertEquals("Wrong username", "username", auth.getUsername()); + Assert.assertArrayEquals("Wrong secret", "supersecret".getBytes(), auth.getSecret()); + Assert.assertEquals("Wrong host", "host.example.com", auth.getHost()); + } +} diff --git a/skaha/src/test/java/org/opencadc/skaha/session/SessionJobBuilderTest.java b/skaha/src/test/java/org/opencadc/skaha/session/SessionJobBuilderTest.java index f4f4fe33..0b415cc0 100644 --- a/skaha/src/test/java/org/opencadc/skaha/session/SessionJobBuilderTest.java +++ b/skaha/src/test/java/org/opencadc/skaha/session/SessionJobBuilderTest.java @@ -4,6 +4,7 @@ import io.kubernetes.client.openapi.models.V1Job; import io.kubernetes.client.openapi.models.V1NodeAffinity; import io.kubernetes.client.openapi.models.V1NodeSelectorRequirement; +import io.kubernetes.client.openapi.models.V1PodSpec; import io.kubernetes.client.util.Yaml; import java.nio.file.Files; import java.nio.file.Path; @@ -56,10 +57,11 @@ public void testWithAffinityMerging() throws Exception { parametersToReplaceValues.put(param, RandomStringUtils.randomAlphanumeric(12)); } - SessionJobBuilder testSubject = SessionJobBuilder.fromPath(testBaseValuesPath) - .withGPUEnabled(true) - .withParameters(parametersToReplaceValues) - .withGPUCount(2); + final SessionJobBuilder testSubject = SessionJobBuilder.fromPath(testBaseValuesPath) + .withGPUEnabled(true) + .withParameters(parametersToReplaceValues) + .withImageSecret("my-secret") + .withGPUCount(2); final String output = testSubject.build(); for (final Map.Entry entry : parametersToReplaceValues.entrySet()) { @@ -68,7 +70,8 @@ public void testWithAffinityMerging() throws Exception { } final V1Job job = (V1Job) Yaml.load(output); - final V1NodeAffinity nodeAffinity = job.getSpec().getTemplate().getSpec().getAffinity().getNodeAffinity(); + final V1PodSpec podSpec = job.getSpec().getTemplate().getSpec(); + final V1NodeAffinity nodeAffinity = podSpec.getAffinity().getNodeAffinity(); final List testMatchExpressions = new ArrayList<>(); final List matchExpressions = @@ -78,6 +81,8 @@ public void testWithAffinityMerging() throws Exception { testMatchExpressions.addAll(matchExpressions); } + Assert.assertEquals("Wrong pull secret.", "my-secret", podSpec.getImagePullSecrets().get(0).getName()); + final V1NodeSelectorRequirement gpuRequirement = new V1NodeSelectorRequirement(); gpuRequirement.setKey("nvidia.com/gpu.count"); gpuRequirement.setOperator("Gt"); diff --git a/skaha/src/test/resources/test-base-values-affinity.yaml b/skaha/src/test/resources/test-base-values-affinity.yaml index 30e4d972..670d07b9 100644 --- a/skaha/src/test/resources/test-base-values-affinity.yaml +++ b/skaha/src/test/resources/test-base-values-affinity.yaml @@ -19,6 +19,8 @@ spec: automountServiceAccountToken: false enableServiceLinks: false restartPolicy: OnFailure + imagePullSecrets: + - name: ${software.imagesecret} affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: From a767f587f3d1d6d73f861de8ee676fd83c630b95 Mon Sep 17 00:00:00 2001 From: Dustin Jenkins Date: Thu, 7 Nov 2024 17:54:51 +0000 Subject: [PATCH 14/15] test: increase test coverage --- .../skaha/utils/CommandExecutioner.java | 9 ++++ .../skaha/utils/CommandExecutionerTest.java | 47 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 skaha/src/test/java/org/opencadc/skaha/utils/CommandExecutionerTest.java diff --git a/skaha/src/main/java/org/opencadc/skaha/utils/CommandExecutioner.java b/skaha/src/main/java/org/opencadc/skaha/utils/CommandExecutioner.java index 46e1d86e..43a6a6d3 100644 --- a/skaha/src/main/java/org/opencadc/skaha/utils/CommandExecutioner.java +++ b/skaha/src/main/java/org/opencadc/skaha/utils/CommandExecutioner.java @@ -129,10 +129,19 @@ public static void ensureRegistrySecret(final ImageRegistryAuth registryAuth, fi } static String[] getDeleteSecretCommand(final String secretName) { + if (!StringUtil.hasText(secretName)) { + throw new IllegalArgumentException("secretName is required."); + } return new String[] {"kubectl", "--namespace", K8SUtil.getWorkloadNamespace(), "delete", "secret", secretName}; } static String[] getRegistryCreateSecretCommand(final ImageRegistryAuth registryAuth, final String secretName) { + if (registryAuth == null) { + throw new IllegalArgumentException("registryAuth is required."); + } else if (!StringUtil.hasText(secretName)) { + throw new IllegalArgumentException("secretName is required."); + } + return new String[] { "kubectl", "--namespace", K8SUtil.getWorkloadNamespace(), "create", "secret", "docker-registry", secretName, diff --git a/skaha/src/test/java/org/opencadc/skaha/utils/CommandExecutionerTest.java b/skaha/src/test/java/org/opencadc/skaha/utils/CommandExecutionerTest.java new file mode 100644 index 00000000..a614a36d --- /dev/null +++ b/skaha/src/test/java/org/opencadc/skaha/utils/CommandExecutionerTest.java @@ -0,0 +1,47 @@ +package org.opencadc.skaha.utils; + +import java.util.Base64; +import org.junit.Assert; +import org.junit.Test; +import org.opencadc.skaha.K8SUtil; +import org.opencadc.skaha.registry.ImageRegistryAuth; + +public class CommandExecutionerTest { + @Test + public void testGetDeleteSecretCommand() { + try { + CommandExecutioner.getDeleteSecretCommand(null); + Assert.fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + Assert.assertEquals("secretName is required.", e.getMessage()); + } + + final String[] deleteCommand = CommandExecutioner.getDeleteSecretCommand("mysecret"); + Assert.assertArrayEquals("Wrong delete command.", new String[] { "kubectl", "--namespace", K8SUtil.getWorkloadNamespace(), "delete", "secret", + "mysecret" }, deleteCommand); + } + + @Test + public void testGetRegistryCreateSecretCommand() { + try { + CommandExecutioner.getRegistryCreateSecretCommand(null, "secret"); + } catch (IllegalArgumentException e) { + Assert.assertEquals("registryAuth is required.", e.getMessage()); + } + + final ImageRegistryAuth imageRegistryAuth = ImageRegistryAuth.fromEncoded(new String(Base64.getEncoder().encode("username:password".getBytes())), + "host"); + + try { + CommandExecutioner.getRegistryCreateSecretCommand(imageRegistryAuth, ""); + } catch (IllegalArgumentException e) { + Assert.assertEquals("secretName is required.", e.getMessage()); + } + + try { + CommandExecutioner.getRegistryCreateSecretCommand(imageRegistryAuth, null); + } catch (IllegalArgumentException e) { + Assert.assertEquals("secretName is required.", e.getMessage()); + } + } +} From 1352f6a0292b3d1061ad888c8a150ea4519c6705 Mon Sep 17 00:00:00 2001 From: Dustin Jenkins Date: Thu, 7 Nov 2024 23:47:42 +0000 Subject: [PATCH 15/15] fix: many bug fixes and cleanup --- .../start-software-sh.template | 13 +- .../helm/skaha/launch-scripts/build-menu.sh | 2 +- .../skaha-config/k8s-resources.properties | 36 ----- .../templates/skaha-config-configmap.yaml | 2 +- image-cache/README.md | 7 + image-cache/VERSION | 6 + .../skaha/DesktopAppLifecycleTest.java | 29 +--- .../opencadc/skaha/ExpiryTimeRenewalTest.java | 65 ++------- .../opencadc/skaha/SessionLifecycleTest.java | 49 +------ .../java/org/opencadc/skaha/SessionUtil.java | 132 +++++++++--------- .../opencadc/skaha/session/PostAction.java | 10 +- skaha/src/main/webapp/service.yaml | 10 +- 12 files changed, 120 insertions(+), 241 deletions(-) delete mode 100644 deployment/helm/skaha/skaha-config/k8s-resources.properties create mode 100644 image-cache/README.md create mode 100644 image-cache/VERSION diff --git a/deployment/helm/skaha/desktop-template/start-software-sh.template b/deployment/helm/skaha/desktop-template/start-software-sh.template index 259735cf..ff357dea 100644 --- a/deployment/helm/skaha/desktop-template/start-software-sh.template +++ b/deployment/helm/skaha/desktop-template/start-software-sh.template @@ -3,7 +3,6 @@ HOST=(HOST) # Callback token TOKEN="${DESKTOP_SESSION_APP_TOKEN}" -CURL_APP_SESSION_HEADERS="--header \"x-auth-token-skaha: ${TOKEN}\"" handle_error() { echo "$1" @@ -19,10 +18,10 @@ get_resource_options() { else resources=`curl -s -L -k --header "x-auth-token-skaha: ${TOKEN}" https://(HOST)/skaha/(SKAHA_API_VERSION)/context` fi - core_default=`echo $resources | jq .defaultCores` - core_options=`echo $resources | jq .availableCores[] | tr '\n' ' '` - ram_default=`echo $resources | jq .defaultRAM` - ram_options=`echo $resources | jq .availableRAM[] | tr '\n' ' '` + core_default=`echo $resources | jq .cores.default` + core_options=`echo $resources | jq .cores.options[] | tr '\n' ' '` + ram_default=`echo $resources | jq .memoryGB.default` + ram_options=`echo $resources | jq .memoryGB.options[] | tr '\n' ' '` } get_cores() { @@ -90,7 +89,7 @@ prompt_user() { if [ -z "${TOKEN}" ]; then handle_error "[skaha] No credentials to call back to Skaha with." else - app_id=`curl -s -L -k "${CURL_APP_SESSION_HEADERS}" -d "image=(IMAGE_ID)" --data-urlencode "param=(NAME)" https://(HOST)/skaha/(SKAHA_API_VERSION)/session/${VNC_PW}/app` + app_id=`curl -s -L -k "x-auth-token-skaha: ${TOKEN}" -d "image=(IMAGE_ID)" --data-urlencode "param=(NAME)" https://(HOST)/skaha/(SKAHA_API_VERSION)/session/${VNC_PW}/app` fi break elif [[ ${yn} == "y" || ${yn} == "Y" ]]; then @@ -102,7 +101,7 @@ prompt_user() { if [ -z "${TOKEN}" ]; then handle_error "[skaha] No credentials to call back to Skaha with." else - app_id=`curl -s -L -k "${CURL_APP_SESSION_HEADERS}" -d "cores=${cores}" -d "ram=$ram" -d "image=(IMAGE_ID)" --data-urlencode "param=(NAME)" https://(HOST)/skaha/(SKAHA_API_VERSION)/session/${VNC_PW}/app` + app_id=`curl -s -L -k "x-auth-token-skaha: ${TOKEN}" -d "cores=${cores}" -d "ram=$ram" -d "image=(IMAGE_ID)" --data-urlencode "param=(NAME)" https://(HOST)/skaha/(SKAHA_API_VERSION)/session/${VNC_PW}/app` fi break else diff --git a/deployment/helm/skaha/launch-scripts/build-menu.sh b/deployment/helm/skaha/launch-scripts/build-menu.sh index d3ec129f..d86169ee 100644 --- a/deployment/helm/skaha/launch-scripts/build-menu.sh +++ b/deployment/helm/skaha/launch-scripts/build-menu.sh @@ -228,7 +228,7 @@ build_menu_item () { echo "[skaha] Start building menu." init create_merged_applications_menu -curl_out=$(curl -s -k --header "x-auth-token-skaha:${TOKEN}" "https://${HOST}/skaha/${SKAHA_API_VERSION}/image?type=desktop-app") +curl_out=$(curl -s -k --header "x-auth-token-skaha: ${TOKEN}" "https://${HOST}/skaha/${SKAHA_API_VERSION}/image?type=desktop-app") if [[ $(echo ${curl_out} | jq '[.[] | .id | length] | add') == 0 ]]; then echo "[skaha] no desktop-app" echo "${curl_out}" diff --git a/deployment/helm/skaha/skaha-config/k8s-resources.properties b/deployment/helm/skaha/skaha-config/k8s-resources.properties deleted file mode 100644 index a7a75c92..00000000 --- a/deployment/helm/skaha/skaha-config/k8s-resources.properties +++ /dev/null @@ -1,36 +0,0 @@ -# Defines the resources available to science containers -### - -# Default number of requested cores when not specified -cores-default-request = 1 - -# Default maximum number of cores when not specified -cores-default-limit = 16 - -# Default number of cores (request=limit) when not specified -cores-default = 2 - -# Default cores for headless jobs -cores-default-headless = 1 - -# Default requested memory (RAM) in GB when not specified -mem-gb-default-request = 4 - -# Default maximum memory (RAM) in GB when not specified -mem-gb-default-limit = 192 - -# Default memory (RAM) in GB (request=limit) when not specified -mem-gb-default = 8 - -# Default RAM for headless jobs -mem-gb-default-headless = 4 - -# Other options for cores -# TODO: Make this a range? -cores-options = 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 - -# Other options for memory (RAM) in GB -mem-gb-options = 1 2 4 6 8 10 12 14 16 20 24 26 28 30 32 36 40 44 48 56 64 80 92 112 128 140 170 192 - - # GPU options -gpus-options = 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 diff --git a/deployment/helm/skaha/templates/skaha-config-configmap.yaml b/deployment/helm/skaha/templates/skaha-config-configmap.yaml index 6087e7ab..66a7f803 100644 --- a/deployment/helm/skaha/templates/skaha-config-configmap.yaml +++ b/deployment/helm/skaha/templates/skaha-config-configmap.yaml @@ -10,6 +10,6 @@ data: {{ base $path }}: | {{- tpl ($.Files.Get $path) $currContext | nindent 4 }} {{ end }} -{{ ($.Files.Glob "skaha-config/*.properties").AsConfig | indent 2 }} +{{ ($.Files.Glob "skaha-config/*.json").AsConfig | indent 2 }} {{- include "utils.extraConfig" (dict "extraConfigData" .Values.deployment.skaha.extraConfigData) -}} {{- (.Files.Glob "image-cache/*").AsConfig | nindent 2 }} diff --git a/image-cache/README.md b/image-cache/README.md new file mode 100644 index 00000000..95a85b85 --- /dev/null +++ b/image-cache/README.md @@ -0,0 +1,7 @@ +# Redis cache client for Image Listings + +This builds a simple image with some formatting tools to support the image caching feature in Skaha. This image acts as a client +to a running Redis cache. See the [cache-images.sh script](https://github.com/opencadc/science-platform/blob/main/deployment/helm/skaha/image-cache/cache-images.sh) in +Skaha, which is run from _within_ this Image. + +See also the [`CronJob` and initialization `Job`](https://github.com/opencadc/science-platform/blob/main/deployment/helm/skaha/templates/image-caching-cronjob.yaml) on how this image is used from a Skaha deployment. \ No newline at end of file diff --git a/image-cache/VERSION b/image-cache/VERSION new file mode 100644 index 00000000..954e1460 --- /dev/null +++ b/image-cache/VERSION @@ -0,0 +1,6 @@ +## deployable containers have a semantic and build tag +# version tag: major.minor.patch +# build version tag: timestamp +VER=0.1.0 +TAGS="${VER} ${VER}-$(date -u +"%Y%m%dT%H%M%S")" +unset VER diff --git a/skaha/src/intTest/java/org/opencadc/skaha/DesktopAppLifecycleTest.java b/skaha/src/intTest/java/org/opencadc/skaha/DesktopAppLifecycleTest.java index dd1b95b8..03d82b5f 100644 --- a/skaha/src/intTest/java/org/opencadc/skaha/DesktopAppLifecycleTest.java +++ b/skaha/src/intTest/java/org/opencadc/skaha/DesktopAppLifecycleTest.java @@ -68,19 +68,12 @@ package org.opencadc.skaha; import ca.nrc.cadc.auth.AuthMethod; -import ca.nrc.cadc.net.HttpGet; import ca.nrc.cadc.reg.Standards; import ca.nrc.cadc.reg.client.RegistryClient; import ca.nrc.cadc.util.Log4jInit; -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; -import java.io.ByteArrayOutputStream; -import java.lang.reflect.Type; import java.net.URL; import java.security.PrivilegedExceptionAction; -import java.util.ArrayList; import java.util.List; -import java.util.concurrent.TimeUnit; import javax.security.auth.Subject; import org.apache.log4j.Level; import org.apache.log4j.Logger; @@ -122,11 +115,12 @@ public DesktopAppLifecycleTest() { public void testCreateDeleteDesktopApp() throws Exception { Subject.doAs(userSubject, (PrivilegedExceptionAction) () -> { // ensure that there is no active session - initialize(); + SessionUtil.initializeCleanup(this.sessionURL); // create desktop session final String desktopSessionID = SessionUtil.createSession(this.sessionURL, "inttest" + SessionAction.SESSION_TYPE_DESKTOP, - SessionUtil.getImageOfType(SessionAction.SESSION_TYPE_DESKTOP).getId()); + SessionUtil.getImageOfType(SessionAction.SESSION_TYPE_DESKTOP).getId(), + SessionAction.SESSION_TYPE_DESKTOP); final Session desktopSession = SessionUtil.waitForSession(this.sessionURL, desktopSessionID, Session.STATUS_RUNNING); SessionUtil.verifySession(desktopSession, SessionAction.SESSION_TYPE_DESKTOP, "inttest" + SessionAction.SESSION_TYPE_DESKTOP); @@ -175,21 +169,4 @@ public void testCreateDeleteDesktopApp() throws Exception { return null; }); } - - private void initialize() throws Exception { - List sessions = SessionUtil.getSessions(this.sessionURL); - for (Session session : sessions) { - if (session.getType().equals(SessionAction.TYPE_DESKTOP_APP)) { - // delete desktop-app - String sessionID = session.getId(); - final URL desktopAppURL = new URL(sessionURL.toString() + "/" + sessionID + "/app"); - SessionUtil.deleteDesktopApplicationSession(desktopAppURL, session.getAppId()); - } else { - // delete session - SessionUtil.deleteSession(sessionURL, session.getId()); - } - } - sessions = SessionUtil.getSessions(this.sessionURL); - Assert.assertEquals("zero sessions #1", 0, sessions.size()); - } } diff --git a/skaha/src/intTest/java/org/opencadc/skaha/ExpiryTimeRenewalTest.java b/skaha/src/intTest/java/org/opencadc/skaha/ExpiryTimeRenewalTest.java index 58dc5c76..3b9f883a 100644 --- a/skaha/src/intTest/java/org/opencadc/skaha/ExpiryTimeRenewalTest.java +++ b/skaha/src/intTest/java/org/opencadc/skaha/ExpiryTimeRenewalTest.java @@ -73,7 +73,6 @@ import ca.nrc.cadc.reg.Standards; import ca.nrc.cadc.reg.client.RegistryClient; import ca.nrc.cadc.util.Log4jInit; - import java.net.MalformedURLException; import java.net.URL; import java.security.PrivilegedExceptionAction; @@ -82,10 +81,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; - import java.util.concurrent.TimeUnit; import javax.security.auth.Subject; - import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.junit.Assert; @@ -98,12 +95,9 @@ */ public class ExpiryTimeRenewalTest { - private static final Logger log = Logger.getLogger(ExpiryTimeRenewalTest.class); - private static final String HOST_PROPERTY = RegistryClient.class.getName() + ".host"; public static final String CARTA_IMAGE_SUFFIX = "/skaha/carta:3.0"; - public static final String PROD_IMAGE_HOST = "images.canfar.net"; - public static final String DEV_IMAGE_HOST = "images-rc.canfar.net"; public static final int SLEEP_TIME_SECONDS = 5; + private static final Logger log = Logger.getLogger(ExpiryTimeRenewalTest.class); static { Log4jInit.setLevel("org.opencadc.skaha", Level.INFO); @@ -111,22 +105,8 @@ public class ExpiryTimeRenewalTest { protected final URL sessionURL; protected final Subject userSubject; - protected final String imageHost; public ExpiryTimeRenewalTest() throws Exception { - // determine image host - String hostP = System.getProperty(HOST_PROPERTY); - if (hostP == null || hostP.trim().isEmpty()) { - throw new IllegalArgumentException("missing server host, check " + HOST_PROPERTY); - } else { - hostP = hostP.trim(); - if (hostP.startsWith("rc-")) { - imageHost = DEV_IMAGE_HOST; - } else { - imageHost = PROD_IMAGE_HOST; - } - } - RegistryClient regClient = new RegistryClient(); final URL sessionServiceURL = regClient.getServiceURL(SessionUtil.getSkahaServiceID(), Standards.PROC_SESSIONS_10, AuthMethod.TOKEN); this.sessionURL = new URL(sessionServiceURL.toString() + "/session"); @@ -140,11 +120,12 @@ public ExpiryTimeRenewalTest() throws Exception { public void testRenewCARTA() throws Exception { Subject.doAs(userSubject, (PrivilegedExceptionAction) () -> { // ensure that there is no active session - initialize(); + SessionUtil.initializeCleanup(this.sessionURL); // create carta session final String cartaSessionID = SessionUtil.createSession(this.sessionURL, "inttest-" + SessionAction.SESSION_TYPE_CARTA, - imageHost + CARTA_IMAGE_SUFFIX); + SessionUtil.getImageByName(ExpiryTimeRenewalTest.CARTA_IMAGE_SUFFIX).getId(), + SessionAction.SESSION_TYPE_CARTA); Session cartaSession = SessionUtil.waitForSession(this.sessionURL, cartaSessionID, Session.STATUS_RUNNING); // Sleep to force time to pass before renewal @@ -181,11 +162,11 @@ public void testRenewHeadless() throws Exception { Subject.doAs(userSubject, (PrivilegedExceptionAction) () -> { // ensure that there is no active session - initialize(); + SessionUtil.initializeCleanup(this.sessionURL); // create headless session final String headlessSessionID = SessionUtil.createHeadlessSession( - SessionUtil.getDesktopAppImageOfType("/skaha/terminal").getId(), this.sessionURL); + SessionUtil.getDesktopAppImageOfType("/skaha/terminal").getId(), this.sessionURL); Session headlessSession = SessionUtil.waitForSession(this.sessionURL, headlessSessionID, Session.STATUS_RUNNING); final Instant headlessExpiryTime = Instant.parse(headlessSession.getExpiryTime()); @@ -210,11 +191,12 @@ public void testRenewHeadless() throws Exception { public void testRenewDesktop() throws Exception { Subject.doAs(userSubject, (PrivilegedExceptionAction) () -> { // ensure that there is no active session - initialize(); + SessionUtil.initializeCleanup(this.sessionURL); // create desktop session final String desktopSessionID = SessionUtil.createSession(this.sessionURL, "inttest-" + SessionAction.SESSION_TYPE_DESKTOP, - SessionUtil.getImageOfType(SessionAction.SESSION_TYPE_DESKTOP).getId()); + SessionUtil.getImageOfType(SessionAction.SESSION_TYPE_DESKTOP).getId(), + SessionAction.SESSION_TYPE_DESKTOP); SessionUtil.waitForSession(this.sessionURL, desktopSessionID, Session.STATUS_RUNNING); // create desktop app @@ -246,39 +228,10 @@ public void testRenewDesktop() throws Exception { }); } - private void initialize() throws Exception { - List sessions = getSessions(); - for (Session session : sessions) { - // skip dekstop-app, deletion of desktop-app is not supported - if (!session.getType().equals(SessionAction.TYPE_DESKTOP_APP)) { - deleteSession(sessionURL, session.getId()); - } - } - - int count = 0; - sessions = getSessions(); - for (Session s : sessions) { - if (!s.getType().equals(SessionAction.TYPE_DESKTOP_APP)) { - count++; - } - } - Assert.assertEquals("zero sessions #1", 0, count); - } - - private void deleteSession(URL sessionURL, String sessionID) throws MalformedURLException { - HttpDelete delete = new HttpDelete(new URL(sessionURL.toString() + "/" + sessionID), true); - delete.run(); - Assert.assertNull("delete session error", delete.getThrowable()); - } - private void renewSession(URL sessionURL, String sessionID) throws Exception { Map params = new HashMap<>(); params.put("action", "renew"); HttpPost post = new HttpPost(new URL(sessionURL.toString() + "/" + sessionID), params, false); post.prepare(); } - - private List getSessions() throws Exception { - return SessionUtil.getSessions(this.sessionURL, Session.STATUS_TERMINATING); - } } diff --git a/skaha/src/intTest/java/org/opencadc/skaha/SessionLifecycleTest.java b/skaha/src/intTest/java/org/opencadc/skaha/SessionLifecycleTest.java index cb2b02f9..e1c22e40 100644 --- a/skaha/src/intTest/java/org/opencadc/skaha/SessionLifecycleTest.java +++ b/skaha/src/intTest/java/org/opencadc/skaha/SessionLifecycleTest.java @@ -73,7 +73,6 @@ import ca.nrc.cadc.util.Log4jInit; import java.net.URL; import java.security.PrivilegedExceptionAction; -import java.util.List; import javax.security.auth.Subject; import org.apache.log4j.Level; import org.apache.log4j.Logger; @@ -87,10 +86,7 @@ */ public class SessionLifecycleTest { - public static final String PROD_IMAGE_HOST = "images.canfar.net"; - public static final String DEV_IMAGE_HOST = "images-rc.canfar.net"; private static final Logger log = Logger.getLogger(SessionLifecycleTest.class); - private static final String HOST_PROPERTY = RegistryClient.class.getName() + ".host"; static { Log4jInit.setLevel("org.opencadc.skaha", Level.INFO); @@ -98,22 +94,8 @@ public class SessionLifecycleTest { protected final URL sessionURL; protected final Subject userSubject; - protected final String imageHost; public SessionLifecycleTest() throws Exception { - // determine image host - String hostP = System.getProperty(HOST_PROPERTY); - if (hostP == null || hostP.trim().isEmpty()) { - throw new IllegalArgumentException("missing server host, check " + HOST_PROPERTY); - } else { - hostP = hostP.trim(); - if (hostP.startsWith("rc-")) { - imageHost = DEV_IMAGE_HOST; - } else { - imageHost = PROD_IMAGE_HOST; - } - } - RegistryClient regClient = new RegistryClient(); final URL sessionServiceURL = regClient.getServiceURL(SessionUtil.getSkahaServiceID(), Standards.PROC_SESSIONS_10, AuthMethod.TOKEN); sessionURL = new URL(sessionServiceURL.toString() + "/session"); @@ -128,18 +110,20 @@ public void testCreateDeleteSessions() throws Exception { Subject.doAs(userSubject, (PrivilegedExceptionAction) () -> { // ensure that there is no active session - initialize(); + SessionUtil.initializeCleanup(this.sessionURL); // create desktop session final String desktopSessionID = SessionUtil.createSession(this.sessionURL, "inttest" + SessionAction.SESSION_TYPE_DESKTOP, - SessionUtil.getImageOfType(SessionAction.SESSION_TYPE_DESKTOP).getId()); + SessionUtil.getImageOfType(SessionAction.SESSION_TYPE_DESKTOP).getId(), + SessionAction.SESSION_TYPE_DESKTOP); final Session desktopSession = SessionUtil.waitForSession(this.sessionURL, desktopSessionID, Session.STATUS_RUNNING); SessionUtil.verifySession(desktopSession, SessionAction.SESSION_TYPE_DESKTOP, "inttest" + SessionAction.SESSION_TYPE_DESKTOP); // create carta session final String cartaSessionID = SessionUtil.createSession(sessionURL, "inttest" + SessionAction.SESSION_TYPE_CARTA, - SessionUtil.getImageOfType(SessionAction.SESSION_TYPE_CARTA).getId()); + SessionUtil.getImageOfType(SessionAction.SESSION_TYPE_CARTA).getId(), + SessionAction.SESSION_TYPE_CARTA); Session cartaSession = SessionUtil.waitForSession(sessionURL, cartaSessionID, Session.STATUS_RUNNING); SessionUtil.verifySession(desktopSession, SessionAction.SESSION_TYPE_CARTA, "inttest" + SessionAction.SESSION_TYPE_CARTA); @@ -166,27 +150,4 @@ public void testCreateDeleteSessions() throws Exception { return null; }); } - - private void initialize() throws Exception { - List sessions = getSessions(); - for (Session session : sessions) { - // skip dekstop-app, deletion of desktop-app is not supported - if (!session.getType().equals(SessionAction.TYPE_DESKTOP_APP)) { - SessionUtil.deleteSession(sessionURL, session.getId()); - } - } - - int count = 0; - sessions = getSessions(); - for (Session s : sessions) { - if (!s.getType().equals(SessionAction.TYPE_DESKTOP_APP)) { - count++; - } - } - Assert.assertEquals("zero sessions #1", 0, count); - } - - private List getSessions() throws Exception { - return SessionUtil.getSessions(sessionURL, Session.STATUS_TERMINATING, Session.STATUS_SUCCEEDED); - } } diff --git a/skaha/src/intTest/java/org/opencadc/skaha/SessionUtil.java b/skaha/src/intTest/java/org/opencadc/skaha/SessionUtil.java index 31e369b0..0a4d8e81 100644 --- a/skaha/src/intTest/java/org/opencadc/skaha/SessionUtil.java +++ b/skaha/src/intTest/java/org/opencadc/skaha/SessionUtil.java @@ -87,49 +87,71 @@ import ca.nrc.cadc.uws.server.RandomStringGenerator; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; +import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.File; -import java.net.PasswordAuthentication; -import java.nio.file.Files; -import java.util.MissingResourceException; -import java.util.concurrent.TimeoutException; -import javax.security.auth.Subject; -import org.apache.log4j.Logger; -import org.junit.Assert; -import org.opencadc.skaha.image.Image; -import org.opencadc.skaha.session.Session; -import org.opencadc.skaha.session.SessionAction; - -import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.StringWriter; import java.io.Writer; import java.lang.reflect.Type; +import java.net.PasswordAuthentication; import java.net.URI; import java.net.URL; +import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.MissingResourceException; +import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; +import javax.security.auth.Subject; +import org.apache.log4j.Logger; +import org.junit.Assert; +import org.opencadc.skaha.image.Image; +import org.opencadc.skaha.session.Session; +import org.opencadc.skaha.session.SessionAction; public class SessionUtil { public static final URI SKAHA_SERVICE_ID = URI.create("ivo://cadc.nrc.ca/skaha"); private static final Logger LOGGER = Logger.getLogger(SessionUtil.class); private static final long ONE_SECOND = 1000L; - private static final long TIMEOUT_WAIT_FOR_SESSION_STARTUP_MS = 45L * SessionUtil.ONE_SECOND; - private static final long TIMEOUT_WAIT_FOR_SESSION_TERMINATE_MS = 40L * SessionUtil.ONE_SECOND; + private static final long TIMEOUT_WAIT_FOR_SESSION_STARTUP_MS = 120L * SessionUtil.ONE_SECOND; + private static final long TIMEOUT_WAIT_FOR_SESSION_TERMINATE_MS = 120L * SessionUtil.ONE_SECOND; static URI getSkahaServiceID() { final String configuredServiceID = System.getenv("SKAHA_SERVICE_ID"); return StringUtil.hasText(configuredServiceID) ? URI.create(configuredServiceID) : SessionUtil.SKAHA_SERVICE_ID; } + static void initializeCleanup(final URL sessionURL) throws Exception { + for (Session session : SessionUtil.getSessions(sessionURL, Session.STATUS_TERMINATING, Session.STATUS_SUCCEEDED)) { + if (session.getType().equals(SessionAction.TYPE_DESKTOP_APP)) { + // delete desktop-app + String sessionID = session.getId(); + final URL desktopAppURL = new URL(sessionURL.toString() + "/" + sessionID + "/app"); + SessionUtil.deleteDesktopApplicationSession(desktopAppURL, session.getAppId()); + } else { + // delete session + SessionUtil.deleteSession(sessionURL, session.getId()); + } + } + + int count = 0; + for (Session s : SessionUtil.getSessions(sessionURL, Session.STATUS_TERMINATING, Session.STATUS_SUCCEEDED)) { + if (!s.getType().equals(SessionAction.TYPE_DESKTOP_APP)) { + count++; + } + } + Assert.assertEquals("zero sessions #1", 0, count); + } + /** * Read in the current user's credentials from the local path. - * @param sessionURL The current URL to use to deduce a domain. - * @return Subject instance, never null. + * + * @param sessionURL The current URL to use to deduce a domain. + * @return Subject instance, never null. */ static Subject getCurrentUser(final URL sessionURL, final boolean allowAnonymous) throws Exception { final Subject subject = new Subject(); @@ -155,8 +177,8 @@ static Subject getCurrentUser(final URL sessionURL, final boolean allowAnonymous final RegistryClient registryClient = new RegistryClient(); URL newLoginURL = registryClient.getServiceURL(URI.create("ivo://cadc.nrc.ca/gms"), Standards.UMS_LOGIN_10, AuthMethod.ANON); final URL loginURL = newLoginURL == null - ? registryClient.getServiceURL(URI.create("ivo://cadc.nrc.ca/gms"), Standards.UMS_LOGIN_01, AuthMethod.ANON) - : newLoginURL; + ? registryClient.getServiceURL(URI.create("ivo://cadc.nrc.ca/gms"), Standards.UMS_LOGIN_01, AuthMethod.ANON) + : newLoginURL; final NetrcFile netrcFile = new NetrcFile(); final PasswordAuthentication passwordAuthentication = netrcFile.getCredentials(loginURL.getHost(), true); final Map loginPayload = new HashMap<>(); @@ -194,17 +216,19 @@ private static X509CertificateChain getProxyCertificate() throws Exception { /** * Create a Session and return the Session ID. Call waitForSession() after to obtain the Session object. - * @param sessionURL The Session URL to use. - * @param name The name of the Session. - * @param image The image URI to use. - * @return String session ID, never null. + * + * @param sessionURL The Session URL to use. + * @param name The name of the Session. + * @param image The image URI to use. + * @return String session ID, never null. */ - static String createSession(final URL sessionURL, final String name, String image) { + static String createSession(final URL sessionURL, final String name, String image, String type) { final Map params = new HashMap<>(); params.put("name", name); params.put("image", image); params.put("cores", 1); params.put("ram", 1); + params.put("type", type); final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); final HttpPost post = new HttpPost(sessionURL, params, outputStream); @@ -216,39 +240,6 @@ static String createSession(final URL sessionURL, final String name, String imag return outputStream.toString().trim(); } - /** - * Start a session and return the ID. - * - * @param image The Image to spin up. - * @param sessionURL The base URL for sessions - * @return String sessionID. - * @throws Exception For any badness. - */ - protected static Session createSession(final String image, final URL sessionURL) throws Exception { - final Map params = new HashMap<>(); - final String name = new RandomStringGenerator(16).getID(); - params.put("name", name); - params.put("image", image); - params.put("cores", 1); - params.put("ram", 1); - - final HttpPost post = new HttpPost(sessionURL, params, false); - post.prepare(); - - final String sessionID; - try (final BufferedReader reader = new BufferedReader(new InputStreamReader(post.getInputStream()))) { - sessionID = reader.readLine(); - } - - final HttpGet httpGet = new HttpGet(new URL(sessionURL.toExternalForm() + "/" + sessionID), true); - httpGet.prepare(); - - final Gson gson = new Gson(); - try (final BufferedReader reader = new BufferedReader(new InputStreamReader(httpGet.getInputStream()))) { - return gson.fromJson(reader, Session.class); - } - } - static String createHeadlessSession(final String image, final URL sessionURL) { final Map params = new HashMap<>(); final String name = new RandomStringGenerator(16).getID(); @@ -274,7 +265,7 @@ static String createDesktopAppSession(final String image, final URL desktopSessi return SessionUtil.createDesktopAppSession(image, desktopSessionURL, 1, 1); } - static String createDesktopAppSession(final String image, final URL desktopSessionURL, final int cores, final int ram) throws Exception { + static String createDesktopAppSession(final String image, final URL desktopSessionURL, final int cores, final int ram) { final Map params = new HashMap<>(); final String name = new RandomStringGenerator(16).getID(); @@ -282,9 +273,12 @@ static String createDesktopAppSession(final String image, final URL desktopSessi params.put("image", image); params.put("cores", cores); params.put("ram", ram); + params.put("type", SessionAction.TYPE_DESKTOP_APP); params.put("cmd", "sleep"); params.put("args", "260"); + LOGGER.info("Creating desktop app session with image " + image + " and name " + name); + final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); final HttpPost post = new HttpPost(desktopSessionURL, params, outputStream); post.setFollowRedirects(false); @@ -305,7 +299,8 @@ static Session waitForDesktopApplicationSession(final URL desktopApplicationURL, currentWaitTime += SessionUtil.ONE_SECOND; if (currentWaitTime > SessionUtil.TIMEOUT_WAIT_FOR_SESSION_STARTUP_MS) { - throw new TimeoutException("Timed out waiting for Desktop Application session " + desktopAppID + " and status " + expectedState); + throw new TimeoutException("Timed out waiting for Desktop Application session " + desktopAppID + " and status " + expectedState + " after " + + currentWaitTime + "ms"); } requestedSession = SessionUtil.getDesktopApplicationSessionWithoutWait(desktopApplicationURL, desktopAppID, expectedState); @@ -377,9 +372,9 @@ static List getAllDesktopApplicationSessions(final URL desktopAppURL) { private static Session getSessionWithoutWait(final URL sessionURL, final String sessionID, final String expectedState) throws Exception { return SessionUtil.getAllSessions(sessionURL).stream() - .filter(session -> session.getId().equals(sessionID) && session.getStatus().equals(expectedState)) - .findFirst() - .orElse(null); + .filter(session -> session.getId().equals(sessionID) && session.getStatus().equals(expectedState)) + .findFirst() + .orElse(null); } static void waitForSessionToTerminate(final URL sessionURL, final String sessionID) throws Exception { @@ -391,7 +386,8 @@ static void waitForSessionToTerminate(final URL sessionURL, final String session currentWaitTime += SessionUtil.ONE_SECOND; if (currentWaitTime > SessionUtil.TIMEOUT_WAIT_FOR_SESSION_TERMINATE_MS) { - throw new TimeoutException("Timed out waiting for session " + sessionID + " and status " + Session.STATUS_TERMINATING); + throw new TimeoutException("Timed out waiting for session " + sessionID + " and status " + Session.STATUS_TERMINATING + " after " + + currentWaitTime + "ms"); } requestedSession = SessionUtil.getSessionWithoutWait(sessionURL, sessionID, Session.STATUS_TERMINATING); @@ -409,7 +405,8 @@ static Session waitForSession(final URL sessionURL, final String sessionID, fina currentWaitTime += SessionUtil.ONE_SECOND; if (currentWaitTime > SessionUtil.TIMEOUT_WAIT_FOR_SESSION_STARTUP_MS) { - throw new TimeoutException("Timed out waiting for session " + sessionID + " and status " + expectedState); + throw new TimeoutException("Timed out waiting for session " + sessionID + " and status " + expectedState + " after " + + currentWaitTime + "ms"); } requestedSession = SessionUtil.getSessionWithoutWait(sessionURL, sessionID, expectedState); @@ -452,6 +449,15 @@ private static List getAllSessions(final URL sessionURL) throws Excepti return gson.fromJson(json, listType); } + static Image getImageByName(final String imagePath) throws Exception { + final RegistryClient registryClient = new RegistryClient(); + final URL imageServiceURL = registryClient.getServiceURL(SessionUtil.getSkahaServiceID(), Standards.PROC_SESSIONS_10, AuthMethod.TOKEN); + final URL imageURL = new URL(imageServiceURL.toExternalForm() + "/image"); + + final List allImagesList = ImagesTest.getImages(imageURL); + return allImagesList.stream().filter(image -> image.getId().endsWith(imagePath)).findFirst().orElseThrow(); + } + protected static Image getImageOfType(final String type) throws Exception { return SessionUtil.getImagesOfType(type).stream().findFirst().orElseThrow(); } diff --git a/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java b/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java index 8bda5da2..ee33228d 100644 --- a/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java +++ b/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java @@ -619,9 +619,8 @@ public void createSession(String type, String image, String name, Integer cores, .withParameter(PostAction.SOFTWARE_LIMITS_RAM, ram + "Gi") .withParameter(PostAction.SKAHA_TLD, this.skahaTld); - if (StringUtil.hasText(supplementalGroups)) { - sessionJobBuilder = sessionJobBuilder.withParameter(PostAction.SKAHA_SUPPLEMENTALGROUPS, supplementalGroups); - } + sessionJobBuilder = sessionJobBuilder.withParameter(PostAction.SKAHA_SUPPLEMENTALGROUPS, StringUtil.hasText(supplementalGroups) + ? supplementalGroups : ""); if (type.equals(SessionAction.SESSION_TYPE_DESKTOP)) { sessionJobBuilder = sessionJobBuilder.withParameter(PostAction.DESKTOP_SESSION_APP_TOKEN, generateToken()); @@ -794,9 +793,8 @@ public void attachDesktopApp(String image, Integer requestCores, Integer limitCo .withParameter(PostAction.SOFTWARE_CONTAINERPARAM, param) .withParameter(PostAction.SKAHA_TLD, this.skahaTld); final String supplementalGroups = getSupplementalGroupsList(); - if (StringUtil.hasText(supplementalGroups)) { - sessionJobBuilder = sessionJobBuilder.withParameter(PostAction.SKAHA_SUPPLEMENTALGROUPS, supplementalGroups); - } + sessionJobBuilder = sessionJobBuilder.withParameter(PostAction.SKAHA_SUPPLEMENTALGROUPS, StringUtil.hasText(supplementalGroups) + ? supplementalGroups : ""); String launchFile = super.stageFile(sessionJobBuilder.build()); String[] launchCmd = new String[] { diff --git a/skaha/src/main/webapp/service.yaml b/skaha/src/main/webapp/service.yaml index c3b8882d..5cbb57c4 100644 --- a/skaha/src/main/webapp/service.yaml +++ b/skaha/src/main/webapp/service.yaml @@ -1,6 +1,6 @@ swagger: '2.0' info: - version: 0.11.0 + version: 0.23.0 title: skaha description: | skaha API Documentation. This API allows authorized users to create and interact with desktop (NoVNC), CARTA Visualization, and Jupyter Notebook sessions in the skaha processing environment. Desktop apps can also be launched and attached to skaha desktop sessions through this API.

Clients may authenticate to this service by:
1. Providing a bearer token in the Authorization header.
2. Using a client certificate.
3. Using a browser cookie from CADC Login.

The main skaha github page with documentation and source code is here: https://github.com/opencadc/science-platform

Documentation on using Skaha can be found here: https://www.opencadc.org/science-containers/ @@ -100,6 +100,14 @@ paths: description: | Only applies to session type 'headless'. Add additional environment to the container. Format is key=value. Multiple env parameters supported. 'key=value' must be URL encoded, so, for example, PATH=/usr/local/bin should be supplied as PATH%3D%2Fusr%2Flocal%2Fbin. required: false + - name: x-skaha-registry-auth + in: header + type: string + description: | + If the image is not in a supported harbor registry, the x-registry-auth header must be provided. This requires additional privileges. The value is a base64 encoded string from "username:secret": + ``` + $ echo -n "username:my-registry-secret" | base64 -i - + ``` responses: '200': description: Successful response