diff --git a/halyard-cli/halyard-cli.gradle b/halyard-cli/halyard-cli.gradle index cebffdd04f..bb29af3a80 100644 --- a/halyard-cli/halyard-cli.gradle +++ b/halyard-cli/halyard-cli.gradle @@ -22,6 +22,8 @@ dependencies { force = true } implementation 'org.nibor.autolink:autolink:0.10.0' + implementation 'org.reflections:reflections:0.9.11' + implementation 'net.minidev:json-smart:1.1.1' implementation project(':halyard-config') implementation project(':halyard-core') diff --git a/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/EnrichMetadata.java b/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/EnrichMetadata.java new file mode 100644 index 0000000000..3b4dedea5c --- /dev/null +++ b/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/EnrichMetadata.java @@ -0,0 +1,66 @@ +package com.netflix.spinnaker.halyard.cli.command.v1; + +import java.lang.reflect.Field; +import java.util.HashMap; + +import com.beust.jcommander.Parameter; +import com.netflix.spinnaker.halyard.config.model.v1.node.Providers; + +import lombok.Data; +import net.minidev.json.JSONObject; +@Data +public class EnrichMetadata { + public JSONObject providersFields = Providers.providersMetadata(); + + public void addProviderCommandFields(String providerName, Field[] fields) { + JSONObject providerFields = (JSONObject) ((JSONObject) providersFields.get("providers")).get(providerName); + if (providersFields != null && providerFields != null) { + for (Field f : fields) { + if (f.getAnnotation(Parameter.class) != null && providerFields.get(f.getName()) != null) { + if (f.getAnnotation(Parameter.class).required() == true) { + ((HashMap) providerFields.get(f.getName())).put("required", true); + } + if (f.getAnnotation(Parameter.class).description() != "") { + ((HashMap) providerFields.get(f.getName())) + .put("description", f.getAnnotation(Parameter.class).description()); + } + } + } + } + } + public void addAccountCommandFields(String providerName, Field[] fields) { + JSONObject accountfields = (JSONObject) ((JSONObject) ((JSONObject) providersFields.get("providers")).get(providerName)) + .get("accountFields"); + if (providersFields != null && accountfields != null) { + for (Field f : fields) { + if (f.getAnnotation(Parameter.class) != null && accountfields.get(f.getName()) != null) { + if (f.getAnnotation(Parameter.class).required() == true) { + ((HashMap) accountfields.get(f.getName())).put("required", true); + } + if (f.getAnnotation(Parameter.class).description() != "") { + ((HashMap) accountfields.get(f.getName())) + .put("description", f.getAnnotation(Parameter.class).description()); + } + } + } + } + } + public void addBakeryCommandFields(String providerName, Field[] fields) { + JSONObject bakeryDefaultsFields = (JSONObject) ((JSONObject) ((JSONObject) providersFields.get("providers")) + .get(providerName)).get("bakeryDefaultsFields"); + if (providersFields != null && bakeryDefaultsFields != null) { + for (Field f : fields) { + if (f.getAnnotation(Parameter.class) != null && bakeryDefaultsFields.get(f.getName()) != null) { + if (f.getAnnotation(Parameter.class).required() == true) { + ((HashMap) bakeryDefaultsFields.get(f.getName())).put("required", true); + } + if (f.getAnnotation(Parameter.class).description() != "") { + ((HashMap) bakeryDefaultsFields.get(f.getName())) + .put("description", f.getAnnotation(Parameter.class).description()); + } + } + } + } + } + +} diff --git a/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/GenerateMetadata.java b/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/GenerateMetadata.java new file mode 100644 index 0000000000..53f82e48de --- /dev/null +++ b/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/GenerateMetadata.java @@ -0,0 +1,116 @@ +package com.netflix.spinnaker.halyard.cli.command.v1; + +import com.netflix.spinnaker.halyard.cli.command.v1.config.providers.AbstractProviderCommand; +import com.netflix.spinnaker.halyard.cli.command.v1.config.providers.account.AbstractAddAccountCommand; +import com.netflix.spinnaker.halyard.cli.command.v1.config.providers.bakery.AbstractEditBakeryDefaultsCommand; +import com.netflix.spinnaker.halyard.config.model.v1.node.Provider.ProviderType; +import com.netflix.spinnaker.halyard.config.model.v1.node.Providers; +import java.lang.reflect.Field; +import java.util.Arrays; +import lombok.Data; +import net.minidev.json.JSONObject; + +@Data +public class GenerateMetadata { + public static JSONObject generateMetadata() { + EnrichMetadata em = new EnrichMetadata(); + for (ProviderType provider : ProviderType.values()) { + try { + Class providerCommandClass = translateProviderCommandType(provider.getName()); + Field[] providerCommandFields = getFields(providerCommandClass); + em.addProviderCommandFields(provider.getName(), providerCommandFields); + + } catch (IllegalArgumentException e) { + // ignoring because it doesn't matter + } + } + for (ProviderType provider : ProviderType.values()) { + try { + Class addBakeryCommandClass = translateEditBakeryCommandType(provider.getName()); + Field[] addBakeryCommandFields = getFields(addBakeryCommandClass); + em.addBakeryCommandFields(provider.getName(), addBakeryCommandFields); + } catch (IllegalArgumentException e) { + // ignoring because it doesn't matter + } + } + for (ProviderType provider : ProviderType.values()) { + try { + Class addCommandClass = translateAddAccountCommandType(provider.getName()); + Field[] addCommandFields = getFields(addCommandClass); + em.addAccountCommandFields(provider.getName(), addCommandFields); + } catch (IllegalArgumentException e) { + // ignoring because it doesn't matter + } + } + return em.getProvidersFields(); + } + + public static Class translateAddAccountCommandType( + String providerName) { + Class providerClass = Providers.translateProviderType(providerName); + + String addAccountCommandClass = + providerClass + .getName() + .replaceAll( + "com.netflix.spinnaker.halyard.config.model.v1.providers", + "com.netflix.spinnaker.halyard.cli.command.v1.config.providers") + .replaceAll("Provider", "AddAccountCommand"); + try { + return (Class) Class.forName(addAccountCommandClass); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException( + "No account for class \"" + addAccountCommandClass + "\" found", e); + } + } + + public static Class translateEditBakeryCommandType( + String providerName) { + Class providerClass = Providers.translateProviderType(providerName); + + String editBakeryDefaultsCommand = + providerClass + .getName() + .replaceAll( + "com.netflix.spinnaker.halyard.config.model.v1.providers", + "com.netflix.spinnaker.halyard.cli.command.v1.config.providers") + .replaceAll("Provider", "EditBakeryDefaultsCommand"); + try { + return (Class) + Class.forName(editBakeryDefaultsCommand); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException( + "No account for class \"" + editBakeryDefaultsCommand + "\" found", e); + } + } + + public static Class translateProviderCommandType( + String providerName) { + Class providerClass = Providers.translateProviderType(providerName); + + String editBakeryDefaultsCommand = + providerClass + .getName() + .replaceAll( + "com.netflix.spinnaker.halyard.config.model.v1.providers", + "com.netflix.spinnaker.halyard.cli.command.v1.config.providers") + .replaceAll("Provider", "EditProviderCommand"); + try { + return (Class) + Class.forName(editBakeryDefaultsCommand); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException( + "No account for class \"" + editBakeryDefaultsCommand + "\" found", e); + } + } + + public static Field[] getFields(Class c) { + Field[] extendedFields = c.getSuperclass().getDeclaredFields(); + Field[] fields = c.getDeclaredFields(); + Field[] allFields = new Field[extendedFields.length + fields.length]; + Arrays.setAll( + allFields, + i -> (i < extendedFields.length ? extendedFields[i] : fields[i - extendedFields.length])); + return allFields; + } +} diff --git a/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/HalCommand.java b/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/HalCommand.java index fdd0c0841e..31aa0b6788 100644 --- a/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/HalCommand.java +++ b/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/HalCommand.java @@ -79,11 +79,9 @@ protected void executeThis() { if (docs) { System.out.println(generateDocs()); } - if (version) { System.out.println(getVersion()); } - if (printBashCompletion) { System.out.println(commandCompletor()); } diff --git a/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/NestableCommand.java b/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/NestableCommand.java index e1391481c5..336b6a1607 100644 --- a/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/NestableCommand.java +++ b/halyard-cli/src/main/java/com/netflix/spinnaker/halyard/cli/command/v1/NestableCommand.java @@ -31,24 +31,36 @@ import com.netflix.spinnaker.halyard.cli.ui.v1.AnsiStoryBuilder; import com.netflix.spinnaker.halyard.cli.ui.v1.AnsiStyle; import com.netflix.spinnaker.halyard.cli.ui.v1.AnsiUi; +import com.netflix.spinnaker.halyard.config.model.v1.node.Account; +import com.netflix.spinnaker.halyard.config.model.v1.node.BakeryDefaults; +import com.netflix.spinnaker.halyard.config.model.v1.node.Providers; import com.netflix.spinnaker.halyard.core.job.v1.JobExecutor; import com.netflix.spinnaker.halyard.core.job.v1.JobExecutorLocal; import com.netflix.spinnaker.halyard.core.resource.v1.StringReplaceJarResource; import java.io.Console; +import java.lang.reflect.Field; import java.net.ConnectException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Comparator; import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.TreeMap; import java.util.concurrent.ThreadLocalRandom; import java.util.stream.Collectors; import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; -import org.nibor.autolink.*; +import net.minidev.json.JSONObject; +import org.nibor.autolink.LinkExtractor; +import org.nibor.autolink.LinkSpan; +import org.nibor.autolink.LinkType; +import org.nibor.autolink.Span; +import org.reflections.Reflections; +import org.reflections.scanners.SubTypesScanner; import retrofit.RetrofitError; @Parameters(separators = "=") @@ -629,7 +641,8 @@ public void configureSubcommands() { commander.addCommand(subCommand.getCommandName(), subCommand); - // We need to provide the subcommand with its own commander before recursively populating its + // We need to provide the subcommand with its own commander before recursively + // populating its // subcommands, since // they need to be registered with this subcommander we retrieve here. JCommander subCommander = commander.getCommands().get(subCommand.getCommandName()); diff --git a/halyard-config/halyard-config.gradle b/halyard-config/halyard-config.gradle index e9913172ea..8cc6b1bf7c 100644 --- a/halyard-config/halyard-config.gradle +++ b/halyard-config/halyard-config.gradle @@ -28,7 +28,8 @@ dependencies { implementation 'io.fabric8:kubernetes-client' implementation 'com.squareup.retrofit:retrofit' implementation 'com.jcraft:jsch' - + implementation 'net.minidev:json-smart:1.1.1' + implementation('com.beust:jcommander:1.71') // TODO: add clouddriverDCOS once that's merged implementation project(':halyard-core') diff --git a/halyard-config/src/main/java/com/netflix/spinnaker/halyard/config/model/v1/node/ProviderDescriptor.java b/halyard-config/src/main/java/com/netflix/spinnaker/halyard/config/model/v1/node/ProviderDescriptor.java new file mode 100644 index 0000000000..61749f79a2 --- /dev/null +++ b/halyard-config/src/main/java/com/netflix/spinnaker/halyard/config/model/v1/node/ProviderDescriptor.java @@ -0,0 +1,108 @@ + +package com.netflix.spinnaker.halyard.config.model.v1.node; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import lombok.Data; +import net.minidev.json.JSONObject; + +@Data +public class ProviderDescriptor { + public JSONObject allProviderFields = new JSONObject(); + public JSONObject providersFields = new JSONObject(); + + public JSONObject initialProviderFields() { + // Fields for most providers: enabled, account, primaryAccount + JSONObject providerFields = new JSONObject(); + Field[] initialFields = Provider.class.getDeclaredFields(); + for (Field field : initialFields) { + providerFields.put(field.getName(), fieldType(field)); + } + return providerFields; + } + + public JSONObject initialAccountFields() { + // Fields for most accounts: name, version ect + JSONObject accountFields = new JSONObject(); + Field[] initialFields = Account.class.getDeclaredFields(); + for (Field field : initialFields) { + accountFields.put(field.getName(), fieldType(field)); + } + return accountFields; + } + + public JSONObject initialBakeryFields() { + JSONObject bakeryFields = new JSONObject(); + Field[] initialFields = BakeryDefaults.class.getDeclaredFields(); + for (Field field : initialFields) { + bakeryFields.put(field.getName(), fieldType(field)); + } + return bakeryFields; + } + + public void addProviderField(String providerName, Field[] extraFields) { + // some providers e.g aws, appengine have extra fields + JSONObject addFields = initialProviderFields(); + for (Field field : extraFields) { + addFields.put(field.getName(), fieldType(field)); + } + if (providerName != null) { + providersFields.put(providerName, addFields); + } + } + + public void addAccountField(String providerName, Field[] fields) { + JSONObject accountFields = initialAccountFields(); + for (Field field : fields) { + accountFields.put(field.getName(), fieldType(field)); + } + if (providersFields != null && providersFields.get(providerName) != null) { + ((HashMap) providersFields.get(providerName)).put("accountFields", accountFields); + + } + } + + public void addBakeryField(String providerName, Field[] fields) { + JSONObject bakeryFields = initialBakeryFields(); + for (Field field : fields) { + bakeryFields.put(field.getName(), fieldType(field)); + } + if (providersFields != null && providersFields.get(providerName) != null) { + ((HashMap) providersFields.get(providerName)).put("useBakeryDefaults", "boolean"); + ((HashMap) providersFields.get(providerName)).put("bakeryDefaultsFields", bakeryFields); + } + } + + public JSONObject allProviderFields() { + allProviderFields.put("providers", providersFields); + return allProviderFields; + } + + public JSONObject fieldType(Field field) { + JSONObject typeWrapper = new JSONObject(); + if (field.getAnnotation(LocalFile.class) != null) { + typeWrapper.put("type", "upload"); + } else if (field.getAnnotation(Secret.class) != null) { + typeWrapper.put("type", "password"); + } else if (field.getType().isEnum()) { + Object[] objects = field.getType().getEnumConstants(); + List enumConstantsList = new ArrayList<>(); + for (Object obj : objects) { + enumConstantsList.add(obj); + } + typeWrapper.put("enum", enumConstantsList); + typeWrapper.put("type", "string"); + + } else if (field.getType() == (List.class) && !field.getName().equals("accounts")) { + // assume a list is a list of strings + typeWrapper.put("type", "stringlist"); + } else { + typeWrapper.put("type", field.getType().getSimpleName().toLowerCase()); + } + return typeWrapper; + } + +} \ No newline at end of file diff --git a/halyard-config/src/main/java/com/netflix/spinnaker/halyard/config/model/v1/node/Providers.java b/halyard-config/src/main/java/com/netflix/spinnaker/halyard/config/model/v1/node/Providers.java index a10737f2a3..a7c943fc47 100644 --- a/halyard-config/src/main/java/com/netflix/spinnaker/halyard/config/model/v1/node/Providers.java +++ b/halyard-config/src/main/java/com/netflix/spinnaker/halyard/config/model/v1/node/Providers.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty.Access; +import com.netflix.spinnaker.halyard.config.model.v1.node.Provider.ProviderType; import com.netflix.spinnaker.halyard.config.model.v1.providers.appengine.AppengineProvider; import com.netflix.spinnaker.halyard.config.model.v1.providers.aws.AwsProvider; import com.netflix.spinnaker.halyard.config.model.v1.providers.azure.AzureProvider; @@ -37,6 +38,7 @@ import java.util.Optional; import lombok.Data; import lombok.EqualsAndHashCode; +import net.minidev.json.JSONObject; @Data @EqualsAndHashCode(callSuper = false) @@ -147,4 +149,31 @@ public static Class translateClusterType(String providerName) "No cluster for class \"" + clusterClassName + "\" found", e); } } + + public static JSONObject providersMetadata() { + ProviderDescriptor pd = new ProviderDescriptor(); + for (ProviderType provider : ProviderType.values()) { + try { + Class providerClass = translateProviderType(provider.getName()); + Class accountClass = translateAccountType(provider.getName()); + Field[] providerFields = providerClass.getDeclaredFields(); + Field[] accountfields = accountClass.getDeclaredFields(); + pd.addProviderField(provider.getName(), providerFields); + pd.addAccountField(provider.getName(), accountfields); + } catch (IllegalArgumentException e) { + // ignoring because it doesn't matter + } + } + for (ProviderType provider : ProviderType.values()) { + try { + Class bakeryClass = translateBakeryDefaultsType(provider.getName()); + Field[] bakeryFields = bakeryClass.getDeclaredFields(); + pd.addBakeryField(provider.getName(), bakeryFields); + + } catch (IllegalArgumentException e) { + // ignoring because it doesn't matter + } + } + return pd.allProviderFields(); + } } diff --git a/halyard-web/src/main/java/com/netflix/spinnaker/halyard/controllers/v1/ConfigController.java b/halyard-web/src/main/java/com/netflix/spinnaker/halyard/controllers/v1/ConfigController.java index 3df51e06ed..06090f2a6e 100644 --- a/halyard-web/src/main/java/com/netflix/spinnaker/halyard/controllers/v1/ConfigController.java +++ b/halyard-web/src/main/java/com/netflix/spinnaker/halyard/controllers/v1/ConfigController.java @@ -16,6 +16,7 @@ package com.netflix.spinnaker.halyard.controllers.v1; +import com.netflix.spinnaker.halyard.cli.command.v1.GenerateMetadata; import com.netflix.spinnaker.halyard.config.config.v1.HalconfigParser; import com.netflix.spinnaker.halyard.config.model.v1.node.Halconfig; import com.netflix.spinnaker.halyard.config.services.v1.ConfigService; @@ -26,9 +27,11 @@ import com.netflix.spinnaker.halyard.core.tasks.v1.DaemonTask; import com.netflix.spinnaker.halyard.core.tasks.v1.DaemonTaskHandler; import lombok.RequiredArgsConstructor; +import net.minidev.json.JSONObject; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; /** Reports the entire contents of ~/.hal/config */ @@ -52,6 +55,12 @@ DaemonTask currentDeployment() { return DaemonTaskHandler.submitTask(builder::build, "Get current deployment"); } + @RequestMapping(value = "/metadata", method = RequestMethod.GET) + @ResponseBody + public JSONObject getMetadata() { + return GenerateMetadata.generateMetadata(); + } + @RequestMapping(value = "/currentDeployment", method = RequestMethod.PUT) DaemonTask setDeployment(@RequestBody StringBodyRequest name) { DaemonResponse.UpdateRequestBuilder builder = new DaemonResponse.UpdateRequestBuilder();