diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/constraints/Constraints.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/constraints/Constraints.java index aa27cf4da..2b070069b 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/constraints/Constraints.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/constraints/Constraints.java @@ -47,6 +47,21 @@ import com.google.common.collect.Multimap; */ public class Constraints { + public static Constraint together(String p1, String p2) { + return and( + implies(p1, p2), + implies(p2, p1) + ); + } + + public static Constraint and(Constraint c1, Constraint c2) { + return (DynamicSchemaContext dc, Node parent, Node node, YType type, IProblemCollector problems) -> { + c1.verify(dc, parent, node, type, problems); + c2.verify(dc, parent, node, type, problems); + }; + } + + public static Constraint implies(String foundProperty, String requiredProperty) { return (DynamicSchemaContext dc, Node parent, Node node, YType type, IProblemCollector problems) -> { if (node instanceof MappingNode) { @@ -209,5 +224,4 @@ public class Constraints { } }; } - } \ No newline at end of file diff --git a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java index 1340da188..955e809d7 100644 --- a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java +++ b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java @@ -724,11 +724,17 @@ public class PipelineYmlSchema implements YamlSchema { AbstractType source = f.ybean("CloudFoundrySource"); addProp(source, "api", t_cf_api_url).isRequired(true); - addProp(source, "username", t_ne_string).isRequired(true); - addProp(source, "password", t_ne_string).isRequired(true); + addProp(source, "username", t_ne_string); + addProp(source, "password", t_ne_string); + addProp(source, "client_id", t_ne_string); + addProp(source, "client_secret", t_ne_string); addProp(source, "organization", t_ne_string).isRequired(true); addProp(source, "space", t_ne_string).isRequired(true); - addProp(source, "skip_cert_check", t_ne_string); + addProp(source, "skip_cert_check", t_boolean); + + source.require(Constraints.together("username", "password")); + source.require(Constraints.together("client_id", "client_secret")); + source.require(Constraints.requireAtLeastOneOf("username", "password", "client_id", "client_secret")); AbstractType get = f.ybean("CloudFoundryGetParams"); //get params deliberately left empty diff --git a/headless-services/concourse-language-server/src/main/resources/desc/CloudFoundrySource/client_id.md b/headless-services/concourse-language-server/src/main/resources/desc/CloudFoundrySource/client_id.md new file mode 100644 index 000000000..a402682c3 --- /dev/null +++ b/headless-services/concourse-language-server/src/main/resources/desc/CloudFoundrySource/client_id.md @@ -0,0 +1 @@ +*Optional*. The client id used to authenticate. diff --git a/headless-services/concourse-language-server/src/main/resources/desc/CloudFoundrySource/client_secret.md b/headless-services/concourse-language-server/src/main/resources/desc/CloudFoundrySource/client_secret.md new file mode 100644 index 000000000..90a498524 --- /dev/null +++ b/headless-services/concourse-language-server/src/main/resources/desc/CloudFoundrySource/client_secret.md @@ -0,0 +1 @@ +*Optional*. The client secret used to authenticate. diff --git a/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java b/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java index 662004b28..8b8d3845d 100644 --- a/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java +++ b/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java @@ -4394,16 +4394,12 @@ public class ConcourseEditorTest { editor.assertContextualCompletions(PLAIN_COMPLETION, "<*>", //Snippet: "api: $1\n" + - " username: $2\n" + - " password: $3\n" + - " organization: $4\n" + - " space: $5<*>" + " organization: $2\n" + + " space: $3<*>" , // non-snippet: "api: <*>", "organization: <*>", - "password: <*>", - "space: <*>", - "username: <*>" + "space: <*>" ); editor = harness.newEditor( @@ -4412,14 +4408,126 @@ public class ConcourseEditorTest { " type: cf\n" + " source:\n" + " api: {{cf_api}}\n" + - " username: {{cf_user}}\n" + - " password: {{cf_password}}\n" + " organization: {{cf_org}}\n" + " space: {{cf_space}}\n" + " <*>" ); editor.assertContextualCompletions(PLAIN_COMPLETION, "<*>", - "skip_cert_check: <*>" + "client_id: <*>", + "client_secret: <*>", + "password: <*>", + "skip_cert_check: <*>", + "username: <*>" + ); + } + + @Test public void cfResourceSourceValidations() throws Exception { + Editor editor; + + editor = harness.newEditor( + "resources:\n" + + "- name: pws\n" + + " type: cf\n" + + " source:\n" + + " api: https://api.run.pivotal.io\n" + + " organization: my-org\n" + + " space: my-space\n" + ); + editor.assertProblems( + "pws|Unused", + "source|One of [username, password, client_id, client_secret] is required" + ); + + editor = harness.newEditor( + "resources:\n" + + "- name: pws\n" + + " type: cf\n" + + " source:\n" + + " api: https://api.run.pivotal.io\n" + + " organization: my-org\n" + + " space: my-space\n" + + " username: myself" + ); + editor.assertProblems( + "pws|Unused", + "username|assumes that 'password' is also defined" + ); + + editor = harness.newEditor( + "resources:\n" + + "- name: pws\n" + + " type: cf\n" + + " source:\n" + + " api: https://api.run.pivotal.io\n" + + " organization: my-org\n" + + " space: my-space\n" + + " password: ((secret))" + ); + editor.assertProblems( + "pws|Unused", + "password|assumes that 'username' is also defined" + ); + + editor = harness.newEditor( + "resources:\n" + + "- name: pws\n" + + " type: cf\n" + + " source:\n" + + " api: https://api.run.pivotal.io\n" + + " organization: my-org\n" + + " space: my-space\n" + + " client_id: ((secret))" + ); + editor.assertProblems( + "pws|Unused", + "client_id|assumes that 'client_secret' is also defined" + ); + + editor = harness.newEditor( + "resources:\n" + + "- name: pws\n" + + " type: cf\n" + + " source:\n" + + " api: https://api.run.pivotal.io\n" + + " organization: my-org\n" + + " space: my-space\n" + + " client_secret: ((secret))" + ); + editor.assertProblems( + "pws|Unused", + "client_secret|assumes that 'client_id' is also defined" + ); + + editor = harness.newEditor( + "resources:\n" + + "- name: pws\n" + + " type: cf\n" + + " source:\n" + + " api: https://api.run.pivotal.io\n" + + " organization: my-org\n" + + " space: my-space\n" + + " client_id: ((secret))\n" + + " client_secret: ((secret))" + ); + editor.assertProblems( + "pws|Unused" + ); + + editor = harness.newEditor( + "resources:\n" + + "- name: pws\n" + + " type: cf\n" + + " source:\n" + + " api: https://api.run.pivotal.io\n" + + " organization: my-org\n" + + " space: my-space\n" + + " username: ((secret))\n" + + " password: ((secret))\n" + + " skip_cert_check: not-bool" + ); + editor.assertProblems( + "pws|Unused", + "not-bool|boolean" ); } @@ -4432,6 +4540,8 @@ public class ConcourseEditorTest { " api: {{cf_api}}\n" + " username: {{cf_user}}\n" + " password: {{cf_password}}\n" + + " client_id: ((cf_client_id))\n" + + " client_secret: ((cf_client_secret))\n" + " organization: {{cf_org}}\n" + " space: {{cf_space}}\n" + " skip_cert_check: true<*>" @@ -4439,6 +4549,8 @@ public class ConcourseEditorTest { editor.assertHoverContains("api", "address of the Cloud Controller"); editor.assertHoverContains("username", "username used to authenticate"); editor.assertHoverContains("password", "password used to authenticate"); + editor.assertHoverContains("client_id", "client id used to authenticate"); + editor.assertHoverContains("client_secret", "client secret used to authenticate"); editor.assertHoverContains("organization", "organization to push"); editor.assertHoverContains("space", "space to push"); editor.assertHoverContains("skip_cert_check", "Check the validity of the CF SSL cert");