V1PodTemplateSpec GetPod(string name, IModuleIdentity identity, KubernetesModule module, IDictionary <string, string> labels)
        {
            // Convert docker labels to annotations because docker labels don't have the same restrictions as Kubernetes labels.
            Dictionary <string, string> annotations = module.Config.CreateOptions.Labels
                                                      .Map(dockerLabels => dockerLabels.ToDictionary(label => KubeUtils.SanitizeAnnotationKey(label.Key), label => label.Value))
                                                      .GetOrElse(() => new Dictionary <string, string>());

            annotations[KubernetesConstants.K8sEdgeOriginalModuleId] = ModuleIdentityHelper.GetModuleName(identity.ModuleId);

            var(proxyContainer, proxyVolumes)   = this.PrepareProxyContainer(module);
            var(moduleContainer, moduleVolumes) = this.PrepareModuleContainer(name, identity, module);

            var imagePullSecrets = new List <Option <string> > {
                this.proxyImagePullSecretName, module.Config.AuthConfig.Map(auth => auth.Name)
            }
            .FilterMap()
            .Distinct()
            .Select(pullSecretName => new V1LocalObjectReference(pullSecretName))
            .ToList();

            V1PodSecurityContext securityContext = module.Config.CreateOptions.SecurityContext.GetOrElse(
                () => this.runAsNonRoot
                    ? new V1PodSecurityContext {
                RunAsNonRoot = true, RunAsUser = 1000
            }
                    : null);

            return(new V1PodTemplateSpec
            {
                Metadata = new V1ObjectMeta
                {
                    Name = name,
                    Labels = labels,
                    Annotations = annotations
                },
                Spec = new V1PodSpec
                {
                    Containers = new List <V1Container> {
                        proxyContainer, moduleContainer
                    },
                    Volumes = proxyVolumes.Concat(moduleVolumes).ToList(),
                    ImagePullSecrets = imagePullSecrets.Any() ? imagePullSecrets : null,
                    SecurityContext = securityContext,
                    ServiceAccountName = name,
                    NodeSelector = module.Config.CreateOptions.NodeSelector.OrDefault()
                }
            });
        }
Example #2
0
 public void SanitizeAnnotationKeyTest(string expected, string raw) => Assert.Equal(expected, KubeUtils.SanitizeAnnotationKey(raw));
Example #3
0
 public void SanitizeAnnotationKeyFailTest(string raw) => Assert.Throws <InvalidKubernetesNameException>(() => KubeUtils.SanitizeAnnotationKey(raw));
        V1PodTemplateSpec GetPod(string name, IModuleIdentity identity, KubernetesModule module, IDictionary <string, string> labels)
        {
            List <V1EnvVar> envVars = this.CollectEnv(module, identity);

            // Convert docker labels to annotations because docker labels don't have the same restrictions as Kubernetes labels.
            Dictionary <string, string> annotations = module.Config.CreateOptions.Labels
                                                      .Map(dockerLabels => dockerLabels.ToDictionary(label => KubeUtils.SanitizeAnnotationKey(label.Key), label => label.Value))
                                                      .GetOrElse(() => new Dictionary <string, string>());

            annotations[KubernetesConstants.K8sEdgeOriginalModuleId] = ModuleIdentityHelper.GetModuleName(identity.ModuleId);

            // Per container settings:
            // exposed ports
            Option <List <V1ContainerPort> > exposedPorts = module.Config.CreateOptions.ExposedPorts
                                                            .Map(PortExtensions.GetContainerPorts);

            // privileged container
            Option <V1SecurityContext> securityContext = module.Config.CreateOptions.HostConfig
                                                         .Filter(config => config.Privileged)
                                                         .Map(config => new V1SecurityContext(privileged: true));

            // Bind mounts
            (List <V1Volume> volumes, List <V1VolumeMount> proxyMounts, List <V1VolumeMount> volumeMounts) = this.GetVolumesFromModule(module);

            var containers = new List <V1Container>
            {
                new V1Container(
                    name,
                    env: envVars,
                    image: module.Config.Image,
                    volumeMounts: volumeMounts,
                    securityContext: securityContext.OrDefault(),
                    ports: exposedPorts.OrDefault(),
                    resources: module.Config.CreateOptions.Resources.OrDefault()),

                new V1Container(
                    "proxy",
                    env: envVars, // TODO: check these for validity for proxy.
                    image: this.proxyImage,
                    volumeMounts: proxyMounts)
            };

            Option <List <V1LocalObjectReference> > imageSecret = module.Config.AuthConfig
                                                                  .Map(auth => new List <V1LocalObjectReference> {
                new V1LocalObjectReference(auth.Name)
            });

            Option <IDictionary <string, string> > nodeSelector = Option.Maybe(module.Config.CreateOptions).FlatMap(options => options.NodeSelector);

            var modulePodSpec = new V1PodSpec(containers, volumes: volumes, imagePullSecrets: imageSecret.OrDefault(), serviceAccountName: name, nodeSelector: nodeSelector.OrDefault());

            var objectMeta = new V1ObjectMeta(name: name, labels: labels, annotations: annotations);

            return(new V1PodTemplateSpec(objectMeta, modulePodSpec));
        }
Example #5
0
        private Option <V1Service> GetServiceFromModule(Dictionary <string, string> labels, KubernetesModule <TConfig> module, IModuleIdentity moduleIdentity)
        {
            var portList = new List <V1ServicePort>();
            Option <Dictionary <string, string> > serviceAnnotations = Option.None <Dictionary <string, string> >();
            bool onlyExposedPorts = true;

            if (module is IModule <AgentDocker.CombinedDockerConfig> moduleWithDockerConfig)
            {
                if (moduleWithDockerConfig.Config.CreateOptions?.Labels != null)
                {
                    // Add annotations from Docker labels. This provides the customer a way to assign annotations to services if they want
                    // to tie backend services to load balancers via an Ingress Controller.
                    var annotations = new Dictionary <string, string>();
                    foreach (KeyValuePair <string, string> label in moduleWithDockerConfig.Config.CreateOptions?.Labels)
                    {
                        annotations.Add(KubeUtils.SanitizeAnnotationKey(label.Key), label.Value);
                    }

                    serviceAnnotations = Option.Some(annotations);
                }

                // Handle ExposedPorts entries
                if (moduleWithDockerConfig.Config?.CreateOptions?.ExposedPorts != null)
                {
                    // Entries in the Exposed Port list just tell Docker that this container wants to listen on that port.
                    // We interpret this as a "ClusterIP" service type listening on that exposed port, backed by this module.
                    // Users of this Module's exposed port should be able to find the service by connecting to "<module name>:<port>"
                    this.GetExposedPorts(moduleWithDockerConfig.Config.CreateOptions.ExposedPorts)
                    .ForEach(
                        exposedList =>
                        exposedList.ForEach((item) => portList.Add(new V1ServicePort(item.Port, name: $"ExposedPort-{item.Port}-{item.Protocol.ToLower()}", protocol: item.Protocol))));
                }

                // Handle HostConfig PortBindings entries
                if (moduleWithDockerConfig.Config?.CreateOptions?.HostConfig?.PortBindings != null)
                {
                    foreach (KeyValuePair <string, IList <DockerModels.PortBinding> > portBinding in moduleWithDockerConfig.Config?.CreateOptions?.HostConfig?.PortBindings)
                    {
                        string[] portProtocol = portBinding.Key.Split('/');
                        if (portProtocol.Length == 2)
                        {
                            if (int.TryParse(portProtocol[0], out int port) && this.ValidateProtocol(portProtocol[1], out string protocol))
                            {
                                // Entries in Docker portMap wants to expose a port on the host (hostPort) and map it to the container's port (port)
                                // We interpret that as the pod wants the cluster to expose a port on a public IP (hostPort), and target it to the container's port (port)
                                foreach (DockerModels.PortBinding hostBinding in portBinding.Value)
                                {
                                    if (int.TryParse(hostBinding.HostPort, out int hostPort))
                                    {
                                        // If a port entry contains the same "port", then remove it and replace with a new ServicePort that contains a target.
                                        var duplicate = portList.SingleOrDefault(a => a.Port == hostPort);
                                        if (duplicate != default(V1ServicePort))
                                        {
                                            portList.Remove(duplicate);
                                        }

                                        portList.Add(new V1ServicePort(hostPort, name: $"HostPort-{port}-{protocol.ToLower()}", protocol: protocol, targetPort: port));
                                        onlyExposedPorts = false;
                                    }
                                    else
                                    {
                                        Events.PortBindingValue(module, portBinding.Key);
                                    }
                                }
                            }
                        }
                    }
                }
            }

            if (portList.Count > 0)
            {
                // Selector: by module name and device name, also how we will label this puppy.
                var objectMeta = new V1ObjectMeta(annotations: serviceAnnotations.GetOrElse(() => null), labels: labels, name: KubeUtils.SanitizeDNSValue(moduleIdentity.ModuleId));
                // How we manage this service is dependent on the port mappings user asks for.
                // If the user tells us to only use ClusterIP ports, we will always set the type to ClusterIP.
                // If all we had were exposed ports, we will assume ClusterIP. Otherwise, we use the given value as the default service type
                //
                // If the user wants to expose the ClusterIPs port externally, they should manually create a service to expose it.
                // This gives the user more control as to how they want this to work.
                string serviceType;
                if (onlyExposedPorts)
                {
                    serviceType = "ClusterIP";
                }
                else
                {
                    serviceType = this.defaultMapServiceType;
                }

                return(Option.Some(new V1Service(metadata: objectMeta, spec: new V1ServiceSpec(type: serviceType, ports: portList, selector: labels))));
            }
            else
            {
                return(Option.None <V1Service>());
            }
        }
Example #6
0
        V1PodTemplateSpec GetPodFromModule(Dictionary <string, string> labels, KubernetesModule <TConfig> module, IModuleIdentity moduleIdentity)
        {
            if (module is IModule <AgentDocker.CombinedDockerConfig> moduleWithDockerConfig)
            {
                // pod labels
                var podLabels = new Dictionary <string, string>(labels);

                // pod annotations
                var podAnnotations = new Dictionary <string, string>();
                podAnnotations.Add(Constants.K8sEdgeOriginalModuleId, moduleIdentity.ModuleId);
                // Convert docker labels to annotations because docker labels don't have the same restrictions as
                // Kuberenetes labels.
                if (moduleWithDockerConfig.Config.CreateOptions?.Labels != null)
                {
                    foreach (KeyValuePair <string, string> label in moduleWithDockerConfig.Config.CreateOptions?.Labels)
                    {
                        podAnnotations.Add(KubeUtils.SanitizeAnnotationKey(label.Key), label.Value);
                    }
                }

                // Per container settings:
                // exposed ports
                Option <List <V1ContainerPort> > exposedPortsOption = (moduleWithDockerConfig.Config?.CreateOptions?.ExposedPorts != null)
                    ? this.GetExposedPorts(moduleWithDockerConfig.Config.CreateOptions.ExposedPorts).Map(
                    servicePorts =>
                    servicePorts.Select(tuple => new V1ContainerPort(tuple.Port, protocol: tuple.Protocol)).ToList())
                    : Option.None <List <V1ContainerPort> >();

                // privileged container
                Option <V1SecurityContext> securityContext = (moduleWithDockerConfig.Config?.CreateOptions?.HostConfig?.Privileged == true) ? Option.Some(new V1SecurityContext(privileged: true)) : Option.None <V1SecurityContext>();

                // Environment Variables.
                List <V1EnvVar> env = this.CollectEnv(moduleWithDockerConfig, (KubernetesModuleIdentity)moduleIdentity);

                // Bind mounts
                (List <V1Volume> volumeList, List <V1VolumeMount> proxyMounts, List <V1VolumeMount> volumeMountList) = this.GetVolumesFromModule(moduleWithDockerConfig).GetOrElse((null, null, null));

                // Image
                string moduleImage = moduleWithDockerConfig.Config.Image;

                var containerList = new List <V1Container>()
                {
                    new V1Container(
                        KubeUtils.SanitizeDNSValue(moduleIdentity.ModuleId),
                        env: env,
                        image: moduleImage,
                        volumeMounts: volumeMountList,
                        securityContext: securityContext.GetOrElse(() => null),
                        ports: exposedPortsOption.GetOrElse(() => null)),

                    // TODO: Add Proxy container here - configmap for proxy configuration.
                    new V1Container(
                        "proxy",
                        env: env, // TODO: check these for validity for proxy.
                        image: this.proxyImage,
                        volumeMounts: proxyMounts)
                };

                Option <List <V1LocalObjectReference> > imageSecret = moduleWithDockerConfig.Config.AuthConfig.Map(
                    auth =>
                {
                    var secret   = new ImagePullSecret(auth);
                    var authList = new List <V1LocalObjectReference>
                    {
                        new V1LocalObjectReference(secret.Name)
                    };
                    return(authList);
                });

                var modulePodSpec = new V1PodSpec(containerList, volumes: volumeList, imagePullSecrets: imageSecret.GetOrElse(() => null));
                var objectMeta    = new V1ObjectMeta(labels: podLabels, annotations: podAnnotations);
                return(new V1PodTemplateSpec(metadata: objectMeta, spec: modulePodSpec));
            }
            else
            {
                Events.InvalidModuleType(module);
            }

            return(new V1PodTemplateSpec());
        }
        Option <V1Service> PrepareService(IModuleIdentity identity, KubernetesModule module, IDictionary <string, string> labels)
        {
            // Add annotations from Docker labels. This provides the customer a way to assign annotations to services if they want
            // to tie backend services to load balancers via an Ingress Controller.
            var annotations = module.Config.CreateOptions.Labels
                              .Map(dockerLabels => dockerLabels.ToDictionary(label => KubeUtils.SanitizeAnnotationKey(label.Key), label => label.Value))
                              .GetOrElse(() => new Dictionary <string, string>());

            // Entries in the Exposed Port list just tell Docker that this container wants to listen on that port.
            // We interpret this as a "ClusterIP" service type listening on that exposed port, backed by this module.
            // Users of this Module's exposed port should be able to find the service by connecting to "<module name>:<port>"
            Option <List <V1ServicePort> > exposedPorts = module.Config.CreateOptions.ExposedPorts
                                                          .Map(ports => ports.GetExposedPorts());

            // Entries in Docker portMap wants to expose a port on the host (hostPort) and map it to the container's port (port)
            // We interpret that as the pod wants the cluster to expose a port on a public IP (hostPort), and target it to the container's port (port)
            Option <List <V1ServicePort> > hostPorts = module.Config.CreateOptions.HostConfig
                                                       .FlatMap(config => Option.Maybe(config.PortBindings).Map(ports => ports.GetHostPorts()));

            bool onlyExposedPorts = !hostPorts.HasValue;

            // override exposed ports with host ports
            var servicePorts = new Dictionary <int, V1ServicePort>();

            exposedPorts.ForEach(ports => ports.ForEach(port => servicePorts[port.Port] = port));
            hostPorts.ForEach(ports => ports.ForEach(port => servicePorts[port.Port]    = port));

            if (servicePorts.Any())
            {
                // Selector: by module name and device name, also how we will label this puppy.
                string name        = identity.DeploymentName();
                var    serviceMeta = new V1ObjectMeta(
                    name: name,
                    labels: labels,
                    annotations: annotations,
                    ownerReferences: module.Owner.ToOwnerReferences());

                // How we manage this service is dependent on the port mappings user asks for.
                // If the user tells us to only use ClusterIP ports, we will always set the type to ClusterIP.
                // If all we had were exposed ports, we will assume ClusterIP. Otherwise, we use the given value as the default service type
                //
                // If the user wants to expose the ClusterIPs port externally, they should manually create a service to expose it.
                // This gives the user more control as to how they want this to work.
                var serviceType = onlyExposedPorts
                    ? PortMapServiceType.ClusterIP
                    : this.defaultMapServiceType;

                return(Option.Some(new V1Service(metadata: serviceMeta, spec: new V1ServiceSpec(type: serviceType.ToString(), ports: servicePorts.Values.ToList(), selector: labels))));
            }

            return(Option.None <V1Service>());
        }
Example #8
0
        V1PodTemplateSpec GetPod(string name, IModuleIdentity identity, KubernetesModule module, IDictionary <string, string> labels)
        {
            // Convert docker labels to annotations because docker labels don't have the same restrictions as Kubernetes labels.
            Dictionary <string, string> annotations = module.Config.CreateOptions.Labels
                                                      .Map(dockerLabels => dockerLabels.ToDictionary(label => KubeUtils.SanitizeAnnotationKey(label.Key), label => label.Value))
                                                      .GetOrElse(() => new Dictionary <string, string>());

            annotations[KubernetesConstants.K8sEdgeOriginalModuleId] = ModuleIdentityHelper.GetModuleName(identity.ModuleId);

            var(proxyContainer, proxyVolumes)   = this.PrepareProxyContainer(module);
            var(moduleContainer, moduleVolumes) = this.PrepareModuleContainer(name, identity, module);

            Option <List <V1LocalObjectReference> > imageSecret = module.Config.AuthConfig
                                                                  .Map(auth => new List <V1LocalObjectReference> {
                new V1LocalObjectReference(auth.Name)
            });

            Option <IDictionary <string, string> > nodeSelector = Option.Maybe(module.Config.CreateOptions).FlatMap(options => options.NodeSelector);

            var modulePodSpec = new V1PodSpec(
                containers: new List <V1Container> {
                proxyContainer, moduleContainer
            },
                volumes: proxyVolumes.Concat(moduleVolumes).ToList(),
                imagePullSecrets: imageSecret.OrDefault(),
                serviceAccountName: name,
                nodeSelector: nodeSelector.OrDefault());

            var objectMeta = new V1ObjectMeta(name: name, labels: labels, annotations: annotations);

            return(new V1PodTemplateSpec(objectMeta, modulePodSpec));
        }