public async Task <Plan> PlanAsync( ModuleSet desired, ModuleSet current, IRuntimeInfo runtimeInfo, IImmutableDictionary <string, IModuleIdentity> moduleIdentities) { Events.LogDesired(desired); Events.LogCurrent(current); Events.LogIdentities(moduleIdentities); // Check that module names sanitize and remain unique. var groupedModules = desired.Modules.GroupBy(pair => KubeUtils.SanitizeK8sValue(pair.Key)).ToArray(); if (groupedModules.Any(c => c.Count() > 1)) { string nameList = groupedModules.Where(c => c.Count() > 1).SelectMany(g => g, (pairs, pair) => pair.Key).Join(","); throw new InvalidIdentityException($"Deployment will cause a name collision in Kubernetes namespace, modules: [{nameList}]"); } // TODO: improve this so it is generic for all potential module types. if (!desired.Modules.Values.All(p => p is IModule <DockerConfig>)) { throw new InvalidModuleException($"Kubernetes deployment currently only handles type={typeof(T).FullName}"); } Diff moduleDifference = desired.Diff(current); Plan plan; if (!moduleDifference.IsEmpty) { // The "Plan" here is very simple - if we have any change, publish all desired modules to a CRD. // The CRD allows us to give the customer a Kubernetes-centric way to see the deployment // and the status of that deployment through the "edgedeployments" API. var k8sModules = desired.Modules.Select(m => new KubernetesModule <DockerConfig>(m.Value as IModule <DockerConfig>)); var crdCommand = new KubernetesCrdCommand <CombinedDockerConfig>(this.deviceNamespace, this.iotHubHostname, this.deviceId, this.client, k8sModules.ToArray(), Option.Some(runtimeInfo), this.combinedConfigProvider as ICombinedConfigProvider <CombinedDockerConfig>); var planCommand = await this.commandFactory.WrapAsync(crdCommand); var planList = new List <ICommand> { planCommand }; Events.PlanCreated(planList); plan = new Plan(planList); } else { plan = Plan.Empty; } return(plan); }
public async void CrdCommandExecuteAsyncInvalidModule() { IModule m1 = new DockerModule("module1", "v1", ModuleStatus.Running, Core.RestartPolicy.Always, Config1, ImagePullPolicy.OnCreate, DefaultConfigurationInfo, EnvVars); var km1 = new KubernetesModule <DockerConfig>(m1 as IModule <DockerConfig>); KubernetesModule <DockerConfig>[] modules = { km1 }; Option <IRuntimeInfo> runtimeOption = Option.Maybe(Runtime); var configProvider = new Mock <ICombinedConfigProvider <CombinedDockerConfig> >(); configProvider.Setup(cp => cp.GetCombinedConfig(km1, Runtime)).Returns(() => null); var token = default(CancellationToken); var cmd = new KubernetesCrdCommand <CombinedDockerConfig>(Ns, Hostname, DeviceId, DefaultClient, modules, runtimeOption, DefaultConfigProvider); await Assert.ThrowsAsync <InvalidModuleException>(() => cmd.ExecuteAsync(token)); }
public async void CrdCommandExecuteWithAuthCreateNewObjects() { IModule m1 = new DockerModule("module1", "v1", ModuleStatus.Running, Core.RestartPolicy.Always, Config1, ImagePullPolicy.OnCreate, DefaultConfigurationInfo, EnvVars); var km1 = new KubernetesModule <DockerConfig>((IModule <DockerConfig>)m1); KubernetesModule <DockerConfig>[] modules = { km1 }; var token = default(CancellationToken); Option <IRuntimeInfo> runtimeOption = Option.Maybe(Runtime); var auth = new AuthConfig() { Username = "******", Password = "******", ServerAddress = "docker.io" }; var configProvider = new Mock <ICombinedConfigProvider <CombinedDockerConfig> >(); configProvider.Setup(cp => cp.GetCombinedConfig(km1, Runtime)).Returns(() => new CombinedDockerConfig("test-image:1", Config1.CreateOptions, Option.Maybe(auth))); bool getSecretCalled = false; bool postSecretCalled = false; bool getCrdCalled = false; bool postCrdCalled = false; using (var server = new MockKubeApiServer( resp: string.Empty, shouldNext: httpContext => { string pathStr = httpContext.Request.Path.Value; string method = httpContext.Request.Method; if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase)) { httpContext.Response.StatusCode = 404; if (pathStr.Contains($"api/v1/namespaces/{Ns}/secrets")) { getSecretCalled = true; } else if (pathStr.Contains($"namespaces/{Ns}/{Constants.K8sCrdPlural}")) { getCrdCalled = true; } } else if (string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase)) { httpContext.Response.StatusCode = 201; httpContext.Response.Body = httpContext.Request.Body; if (pathStr.Contains($"api/v1/namespaces/{Ns}/secrets")) { postSecretCalled = true; } else if (pathStr.Contains($"namespaces/{Ns}/{Constants.K8sCrdPlural}")) { postCrdCalled = true; } } return(Task.FromResult(false)); })) { var client = new Kubernetes( new KubernetesClientConfiguration { Host = server.Uri.ToString() }); var cmd = new KubernetesCrdCommand <CombinedDockerConfig>(Ns, Hostname, DeviceId, client, modules, runtimeOption, configProvider.Object); await cmd.ExecuteAsync(token); Assert.True(getSecretCalled, nameof(getSecretCalled)); Assert.True(postSecretCalled, nameof(postSecretCalled)); Assert.True(getCrdCalled, nameof(getCrdCalled)); Assert.True(postCrdCalled, nameof(postCrdCalled)); } }
public async void CrdCommandExecuteTwoModulesWithSamePullSecret() { string resourceName = Hostname + Constants.K8sNameDivider + DeviceId.ToLower(); string secretName = "username-docker.io"; IModule m1 = new DockerModule("module1", "v1", ModuleStatus.Running, Core.RestartPolicy.Always, Config1, ImagePullPolicy.OnCreate, DefaultConfigurationInfo, EnvVars); var km1 = new KubernetesModule <DockerConfig>((IModule <DockerConfig>)m1); IModule m2 = new DockerModule("module2", "v1", ModuleStatus.Running, Core.RestartPolicy.Always, Config2, ImagePullPolicy.OnCreate, DefaultConfigurationInfo, EnvVars); var km2 = new KubernetesModule <DockerConfig>((IModule <DockerConfig>)m2); KubernetesModule <DockerConfig>[] modules = { km1, km2 }; var token = default(CancellationToken); Option <IRuntimeInfo> runtimeOption = Option.Maybe(Runtime); var auth = new AuthConfig() { Username = "******", Password = "******", ServerAddress = "docker.io" }; var configProvider = new Mock <ICombinedConfigProvider <CombinedDockerConfig> >(); configProvider.Setup(cp => cp.GetCombinedConfig(It.IsAny <KubernetesModule <DockerConfig> >(), Runtime)).Returns(() => new CombinedDockerConfig("test-image:1", Config1.CreateOptions, Option.Maybe(auth))); bool getSecretCalled = false; bool putSecretCalled = false; int postSecretCalled = 0; bool getCrdCalled = false; bool putCrdCalled = false; int postCrdCalled = 0; Stream secretBody = Stream.Null; using (var server = new MockKubeApiServer( resp: string.Empty, shouldNext: httpContext => { string pathStr = httpContext.Request.Path.Value; string method = httpContext.Request.Method; if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase)) { if (pathStr.Contains($"api/v1/namespaces/{Ns}/secrets/{secretName}")) { if (secretBody == Stream.Null) { // 1st pass, secret should not exist getSecretCalled = true; httpContext.Response.StatusCode = 404; } else { // 2nd pass, use secret from creation. httpContext.Response.Body = secretBody; } } else if (pathStr.Contains($"namespaces/{Ns}/{Constants.K8sCrdPlural}/{resourceName}")) { getCrdCalled = true; httpContext.Response.StatusCode = 404; } } else if (string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase)) { httpContext.Response.StatusCode = 201; httpContext.Response.Body = httpContext.Request.Body; if (pathStr.Contains($"api/v1/namespaces/{Ns}/secrets")) { postSecretCalled++; secretBody = httpContext.Request.Body; // save this for next query. } else if (pathStr.Contains($"namespaces/{Ns}/{Constants.K8sCrdPlural}")) { postCrdCalled++; } } else if (string.Equals(method, "PUT", StringComparison.OrdinalIgnoreCase)) { httpContext.Response.Body = httpContext.Request.Body; if (pathStr.Contains($"api/v1/namespaces/{Ns}/secrets")) { putSecretCalled = true; } else if (pathStr.Contains($"namespaces/{Ns}/{Constants.K8sCrdPlural}")) { putCrdCalled = true; } } return(Task.FromResult(false)); })) { var client = new Kubernetes( new KubernetesClientConfiguration { Host = server.Uri.ToString() }); var cmd = new KubernetesCrdCommand <CombinedDockerConfig>(Ns, Hostname, DeviceId, client, modules, runtimeOption, configProvider.Object); await cmd.ExecuteAsync(token); Assert.True(getSecretCalled, nameof(getSecretCalled)); Assert.Equal(1, postSecretCalled); Assert.False(putSecretCalled, nameof(putSecretCalled)); Assert.True(getCrdCalled, nameof(getCrdCalled)); Assert.Equal(1, postCrdCalled); Assert.False(putCrdCalled, nameof(putCrdCalled)); } }
public async void CrdCommandExecuteWithAuthReplaceObjects() { string resourceName = Hostname + Constants.K8sNameDivider + DeviceId.ToLower(); string metaApiVersion = Constants.K8sApi + "/" + Constants.K8sApiVersion; string secretName = "username-docker.io"; var secretData = new Dictionary <string, byte[]> { [Constants.K8sPullSecretData] = Encoding.UTF8.GetBytes("Invalid Secret Data") }; var secretMeta = new V1ObjectMeta(name: secretName, namespaceProperty: Ns); IModule m1 = new DockerModule("module1", "v1", ModuleStatus.Running, Core.RestartPolicy.Always, Config1, ImagePullPolicy.OnCreate, DefaultConfigurationInfo, EnvVars); var km1 = new KubernetesModule <DockerConfig>((IModule <DockerConfig>)m1); KubernetesModule <DockerConfig>[] modules = { km1 }; var token = default(CancellationToken); Option <IRuntimeInfo> runtimeOption = Option.Maybe(Runtime); var auth = new AuthConfig() { Username = "******", Password = "******", ServerAddress = "docker.io" }; var configProvider = new Mock <ICombinedConfigProvider <CombinedDockerConfig> >(); configProvider.Setup(cp => cp.GetCombinedConfig(km1, Runtime)).Returns(() => new CombinedDockerConfig("test-image:1", Config1.CreateOptions, Option.Maybe(auth))); var existingSecret = new V1Secret("v1", secretData, type: Constants.K8sPullSecretType, kind: "Secret", metadata: secretMeta); var existingDeployment = new EdgeDeploymentDefinition <DockerConfig>(metaApiVersion, Constants.K8sCrdKind, new V1ObjectMeta(name: resourceName), new List <KubernetesModule <DockerConfig> >()); bool getSecretCalled = false; bool putSecretCalled = false; bool getCrdCalled = false; bool putCrdCalled = false; using (var server = new MockKubeApiServer( resp: string.Empty, shouldNext: async httpContext => { string pathStr = httpContext.Request.Path.Value; string method = httpContext.Request.Method; if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase)) { if (pathStr.Contains($"api/v1/namespaces/{Ns}/secrets/{secretName}")) { getSecretCalled = true; await httpContext.Response.Body.WriteAsync(JsonConvert.SerializeObject(existingSecret).ToBody(), token); } else if (pathStr.Contains($"namespaces/{Ns}/{Constants.K8sCrdPlural}/{resourceName}")) { getCrdCalled = true; await httpContext.Response.Body.WriteAsync(JsonConvert.SerializeObject(existingDeployment).ToBody(), token); } } else if (string.Equals(method, "PUT", StringComparison.OrdinalIgnoreCase)) { httpContext.Response.Body = httpContext.Request.Body; if (pathStr.Contains($"api/v1/namespaces/{Ns}/secrets/{secretName}")) { putSecretCalled = true; } else if (pathStr.Contains($"namespaces/{Ns}/{Constants.K8sCrdPlural}/{resourceName}")) { putCrdCalled = true; } } return(false); })) { var client = new Kubernetes( new KubernetesClientConfiguration { Host = server.Uri.ToString() }); var cmd = new KubernetesCrdCommand <CombinedDockerConfig>(Ns, Hostname, DeviceId, client, modules, runtimeOption, configProvider.Object); await cmd.ExecuteAsync(token); Assert.True(getSecretCalled, nameof(getSecretCalled)); Assert.True(putSecretCalled, nameof(putSecretCalled)); Assert.True(getCrdCalled, nameof(getCrdCalled)); Assert.True(putCrdCalled, nameof(putCrdCalled)); } }