/// <summary>
        /// ユーザの任意追加環境変数をマージする
        /// </summary>
        private static void AddUserEnvToInputModel(Dictionary <string, string> optionDic, RunContainerInputModel inputModel)
        {
            if (optionDic.Count > 0)
            {
                foreach (var env in optionDic)
                {
                    // ユーザー指定環境変数とappsettingsの環境変数を結合

                    string value = env.Value ?? ""; //nullは空文字と見なす

                    if (inputModel.EnvList.ContainsKey(env.Key))
                    {
                        inputModel.EnvList[env.Key] = value; //あればユーザ指定の値で上書き
                    }
                    else
                    {
                        inputModel.EnvList.Add(env.Key, value); //なければ追加
                    }
                }
            }
        }
        /// <summary>
        /// 新規に画像認識の推論用コンテナを作成する。
        /// </summary>
        /// <param name="inferenceHistory">対象の推論履歴</param>
        /// <returns>作成したコンテナのステータス</returns>
        public async Task <Result <ContainerInfo, string> > RunInferenceContainerAsync(InferenceHistory inferenceHistory)
        {
            string token = await GetUserAccessTokenAsync();

            if (token == null)
            {
                //トークンがない場合、結果はnull
                return(Result <ContainerInfo, string> .CreateErrorResult("Access denied. Failed to get token to access the cluster management system."));
            }

            long gitId = inferenceHistory.ModelGitId == -1 ?
                         CurrentUserInfo.SelectedTenant.DefaultGitId.Value : inferenceHistory.ModelGitId.Value;

            var registryMap = registryLogic.GetCurrentRegistryMap(inferenceHistory.ContainerRegistryId.Value);
            var gitEndpoint = gitLogic.GetPullUrl(gitId, inferenceHistory.ModelRepository, inferenceHistory.ModelRepositoryOwner);

            if (gitEndpoint == null)
            {
                //Git情報は必須にしているので、無ければエラー
                return(Result <ContainerInfo, string> .CreateErrorResult("Git credential is not valid."));
            }

            var nodes = GetAccessibleNode();

            if (nodes == null || nodes.Count == 0)
            {
                //デプロイ可能なノードがゼロなら、エラー扱い
                return(Result <ContainerInfo, string> .CreateErrorResult("Access denied. There is no node this tenant can use."));
            }


            //コンテナを起動するために必要な設定値をインスタンス化
            var inputModel = new RunContainerInputModel()
            {
                ID              = inferenceHistory.Id,
                TenantName      = TenantName,
                LoginUser       = CurrentUserInfo.Alias, //アカウントはエイリアスから指定
                Name            = inferenceHistory.Key,
                ContainerImage  = registryMap.Registry.GetImagePath(inferenceHistory.ContainerImage, inferenceHistory.ContainerTag),
                ScriptType      = "inference",
                Cpu             = inferenceHistory.Cpu,
                Memory          = inferenceHistory.Memory,
                Gpu             = inferenceHistory.Gpu,
                KqiImage        = "kamonohashi/cli:" + versionLogic.GetVersion(),
                KqiToken        = loginLogic.GenerateToken().AccessToken,
                LogPath         = "/kqi/attach/inference_stdout_stderr_${INFERENCE_ID}.log",
                NfsVolumeMounts = new List <NfsVolumeMountModel>()
                {
                    // 結果保存するディレクトリ
                    new NfsVolumeMountModel()
                    {
                        Name       = "nfs-output",
                        MountPath  = "/kqi/output",
                        SubPath    = inferenceHistory.Id.ToString(),
                        Server     = CurrentUserInfo.SelectedTenant.Storage.NfsServer,
                        ServerPath = CurrentUserInfo.SelectedTenant.InferenceContainerOutputNfsPath,
                        ReadOnly   = false
                    },
                    // 添付ファイルを保存するディレクトリ
                    // 学習結果ディレクトリを学習完了時にzip圧縮して添付するために使用
                    new NfsVolumeMountModel()
                    {
                        Name       = "nfs-attach",
                        MountPath  = "/kqi/attach",
                        SubPath    = inferenceHistory.Id.ToString(),
                        Server     = CurrentUserInfo.SelectedTenant.Storage.NfsServer,
                        ServerPath = CurrentUserInfo.SelectedTenant.InferenceContainerAttachedNfsPath,
                        ReadOnly   = false
                    }
                },
                ContainerSharedPath = new Dictionary <string, string>()
                {
                    { "tmp", "/kqi/tmp/" },
                    { "input", "/kqi/input/" },
                    { "git", "/kqi/git/" }
                },
                EnvList = new Dictionary <string, string>()
                {
                    { "DATASET_ID", inferenceHistory.DataSetId.ToString() },
                    { "INFERENCE_ID", inferenceHistory.Id.ToString() },
                    { "PARENT_ID", inferenceHistory.ParentId?.ToString() },
                    { "MODEL_REPOSITORY", gitEndpoint.FullUrl },
                    { "MODEL_REPOSITORY_URL", gitEndpoint.Url },
                    { "MODEL_REPOSITORY_TOKEN", gitEndpoint.Token },
                    { "COMMIT_ID", inferenceHistory.ModelCommitId },
                    { "KQI_SERVER", containerOptions.WebServerUrl },
                    { "KQI_TOKEN", loginLogic.GenerateToken().AccessToken },
                    { "http_proxy", containerOptions.Proxy },
                    { "https_proxy", containerOptions.Proxy },
                    { "no_proxy", containerOptions.NoProxy },
                    { "HTTP_PROXY", containerOptions.Proxy },
                    { "HTTPS_PROXY", containerOptions.Proxy },
                    { "NO_PROXY", containerOptions.NoProxy },
                    { "COLUMNS", containerOptions.ShellColumns },
                    { "PYTHONUNBUFFERED", "true" }, // python実行時の標準出力・エラーのバッファリングをなくす
                    { "LC_ALL", "C.UTF-8" },        // python実行時のエラー回避
                    { "LANG", "C.UTF-8" }  // python実行時のエラー回避
                },
                EntryPoint = inferenceHistory.EntryPoint,

                PortMappings = new PortMappingModel[]
                {
                    new PortMappingModel()
                    {
                        Protocol = "TCP", Port = 22, TargetPort = 22, Name = "ssh"
                    },
                },
                ClusterManagerToken = token,
                RegistryTokenName   = registryMap.RegistryTokenKey,
                IsNodePort          = true
            };

            // 親を指定した場合は親の出力結果を/kqi/parentにマウント
            // 推論ジョブにおける親ジョブは学習ジョブとなるので、SubPathとServerPathの指定に注意
            if (inferenceHistory.ParentId != null)
            {
                inputModel.NfsVolumeMounts.Add(new NfsVolumeMountModel()
                {
                    Name       = "nfs-parent",
                    MountPath  = "/kqi/parent",
                    SubPath    = inferenceHistory.ParentId.ToString(),
                    Server     = CurrentUserInfo.SelectedTenant.Storage.NfsServer,
                    ServerPath = CurrentUserInfo.SelectedTenant.TrainingContainerOutputNfsPath,
                    ReadOnly   = true
                });
            }

            // ユーザの任意追加環境変数をマージする
            AddUserEnvToInputModel(inferenceHistory.OptionDic, inputModel);

            //使用できるノードを制約に追加
            inputModel.ConstraintList = new Dictionary <string, List <string> >()
            {
                { containerOptions.ContainerLabelHostName, nodes }
            };

            if (string.IsNullOrEmpty(inferenceHistory.Partition) == false)
            {
                // パーティション指定があれば追加
                inputModel.ConstraintList.Add(containerOptions.ContainerLabelPartition, new List <string> {
                    inferenceHistory.Partition
                });
            }

            var outModel = await clusterManagementService.RunContainerAsync(inputModel);

            if (outModel.IsSuccess == false)
            {
                return(Result <ContainerInfo, string> .CreateErrorResult(outModel.Error));
            }
            var port = outModel.Value.PortMappings.Find(p => p.Name == "ssh");

            return(Result <ContainerInfo, string> .CreateResult(new ContainerInfo()
            {
                Name = outModel.Value.Name,
                Status = outModel.Value.Status,
                Host = outModel.Value.Host,
                Port = port.NodePort,
                Configuration = outModel.Value.Configuration
            }));
        }
        /// <summary>
        /// 新規にTensorBoard表示用のコンテナを作成する。
        /// 成功した場合は作成結果が、失敗した場合はnullが返る。
        /// </summary>
        /// <param name="trainingHistory">対象の学習履歴</param>
        /// <returns>作成したコンテナのステータス</returns>
        public async Task <ContainerInfo> RunTensorBoardContainerAsync(TrainingHistory trainingHistory)
        {
            //コンテナ名は自動生成
            //使用できる文字など、命名規約はコンテナ管理サービス側によるが、
            //ユーザ入力値検証の都合でどうせ決め打ちしないといけないので、ロジック層で作ってしまう
            string tenantId      = CurrentUserInfo.SelectedTenant.Id.ToString("0000");
            string containerName = $"tensorboard-{tenantId}-{trainingHistory.Id}-{DateTime.Now.ToString("yyyyMMddHHmmssffffff")}";
            var    registryMap   = registryLogic.GetCurrentRegistryMap(trainingHistory.ContainerRegistryId.Value);

            string token = await GetUserAccessTokenAsync();

            if (token == null)
            {
                //トークンがない場合、結果はnull
                return(new ContainerInfo()
                {
                    Status = ContainerStatus.Forbidden
                });
            }

            var nodes = GetAccessibleNode();

            if (nodes == null || nodes.Count == 0)
            {
                //デプロイ可能なノードがゼロなら、エラー扱い
                return(new ContainerInfo()
                {
                    Status = ContainerStatus.Forbidden
                });
            }

            //コンテナを起動するために必要な設定値をインスタンス化
            var inputModel = new RunContainerInputModel()
            {
                ID              = trainingHistory.Id,
                TenantName      = TenantName,
                LoginUser       = CurrentUserInfo.Alias, //アカウントはエイリアスから指定
                Name            = containerName,
                ContainerImage  = "tensorflow/tensorflow",
                ScriptType      = "tensorboard",
                Cpu             = 1,
                Memory          = 1, //メモリは1GBで仮決め
                Gpu             = 0,
                KqiImage        = "kamonohashi/cli:" + versionLogic.GetVersion(),
                NfsVolumeMounts = new List <NfsVolumeMountModel>()
                {
                    // 結果が保存されているディレクトリ
                    new NfsVolumeMountModel()
                    {
                        Name       = "nfs-output",
                        MountPath  = "/kqi/output",
                        SubPath    = trainingHistory.Id.ToString(),
                        Server     = CurrentUserInfo.SelectedTenant.Storage.NfsServer,
                        ServerPath = CurrentUserInfo.SelectedTenant.TrainingContainerOutputNfsPath
                    }
                },
                EnvList = new Dictionary <string, string>()
                {
                    { "KQI_SERVER", containerOptions.WebServerUrl },
                    { "KQI_TOKEN", loginLogic.GenerateToken().AccessToken },
                    { "http_proxy", containerOptions.Proxy },
                    { "https_proxy", containerOptions.Proxy },
                    { "no_proxy", containerOptions.NoProxy },
                    { "HTTP_PROXY", containerOptions.Proxy },
                    { "HTTPS_PROXY", containerOptions.Proxy },
                    { "NO_PROXY", containerOptions.NoProxy },
                    { "PYTHONUNBUFFERED", "true" }, // python実行時の標準出力・エラーのバッファリングをなくす
                    { "LC_ALL", "C.UTF-8" },        // python実行時のエラー回避
                    { "LANG", "C.UTF-8" }  // python実行時のエラー回避
                },
                ConstraintList = new Dictionary <string, List <string> >()
                {
                    { containerOptions.ContainerLabelHostName, nodes }, //使用できるノードを取得し、制約に追加
                    { containerOptions.ContainerLabelTensorBoardEnabled, new List <string> {
                          "true"
                      } }                                                                              // tensorboardの実行が許可されているサーバでのみ実行,
                },
                PortMappings = new PortMappingModel[]
                {
                    new PortMappingModel()
                    {
                        Protocol = "TCP", Port = 6006, TargetPort = 6006, Name = "tensorboard"
                    }
                },
                ClusterManagerToken = token,
                RegistryTokenName   = registryMap.RegistryTokenKey,
                IsNodePort          = true //ランダムポート指定。アクセス先ポートが動的に決まるようになる。
            };

            var outModel = await clusterManagementService.RunContainerAsync(inputModel);

            if (outModel.IsSuccess == false)
            {
                return(new ContainerInfo()
                {
                    Status = ContainerStatus.Failed
                });
            }
            var port = outModel.Value.PortMappings.Find(p => p.Name == "tensorboard");

            return(new ContainerInfo()
            {
                Name = containerName,
                Status = outModel.Value.Status,
                Host = outModel.Value.Host,
                Port = port.NodePort,
                Configuration = outModel.Value.Configuration
            });
        }
        /// <summary>
        /// 新規に前処理用コンテナを作成する。
        /// </summary>
        /// <param name="preprocessHistory">対象の前処理履歴</param>
        /// <returns>作成したコンテナのステータス</returns>
        public async Task <Result <ContainerInfo, string> > RunPreprocessingContainerAsync(PreprocessHistory preprocessHistory)
        {
            string token = await GetUserAccessTokenAsync();

            if (token == null)
            {
                //トークンがない場合、結果はnull
                return(Result <ContainerInfo, string> .CreateErrorResult("Access denied. Failed to get token to access the cluster management system."));
            }

            var registryMap = registryLogic.GetCurrentRegistryMap(preprocessHistory.Preprocess.ContainerRegistryId.Value);

            string tags = "-t " + preprocessHistory.Preprocess.Name; //生成されるデータのタグを設定

            foreach (var tag in preprocessHistory.InputData.Tags)
            {
                tags += " -t " + tag;
            }

            //コンテナを起動するために必要な設定値をインスタンス化
            var inputModel = new RunContainerInputModel()
            {
                ID              = preprocessHistory.Id,
                TenantName      = TenantName,
                LoginUser       = CurrentUserInfo.Alias, //アカウントはエイリアスから指定
                Name            = preprocessHistory.Name,
                ContainerImage  = registryMap.Registry.GetImagePath(preprocessHistory.Preprocess.ContainerImage, preprocessHistory.Preprocess.ContainerTag),
                ScriptType      = "preproc", // 実行スクリプトの指定
                Cpu             = preprocessHistory.Cpu.Value,
                Memory          = preprocessHistory.Memory.Value,
                Gpu             = preprocessHistory.Gpu.Value,
                KqiToken        = loginLogic.GenerateToken().AccessToken,
                KqiImage        = "kamonohashi/cli:" + versionLogic.GetVersion(),
                LogPath         = "/kqi/attach/preproc_stdout_stderr_${PREPROCESSING_ID}_${DATA_ID}.log", // 前処理履歴IDは現状ユーザーに見えないので前処理+データIDをつける
                NfsVolumeMounts = new List <NfsVolumeMountModel>()
                {
                    // 添付ファイルを保存するディレクトリ
                    // 前処理結果ディレクトリを前処理完了時にzip圧縮して添付するために使用
                    new NfsVolumeMountModel()
                    {
                        Name       = "nfs-preproc-attach",
                        MountPath  = "/kqi/attach",
                        SubPath    = preprocessHistory.Id.ToString(),
                        Server     = CurrentUserInfo.SelectedTenant.Storage.NfsServer,
                        ServerPath = CurrentUserInfo.SelectedTenant.PreprocContainerAttachedNfsPath,
                        ReadOnly   = false
                    }
                },
                ContainerSharedPath = new Dictionary <string, string>()
                {
                    { "tmp", "/kqi/tmp/" },
                    { "input", "/kqi/input/" },
                    { "git", "/kqi/git/" },
                    { "output", "/kqi/output/" }
                },
                EnvList = new Dictionary <string, string>()
                {
                    { "DATA_ID", preprocessHistory.InputDataId.ToString() },
                    { "DATA_NAME", preprocessHistory.InputData.Name },
                    { "PREPROCESSING_ID", preprocessHistory.PreprocessId.ToString() },
                    { "TAGS", tags },
                    { "COMMIT_ID", preprocessHistory.Preprocess.RepositoryCommitId },
                    { "KQI_SERVER", containerOptions.WebServerUrl },
                    { "KQI_TOKEN", loginLogic.GenerateToken().AccessToken },
                    { "http_proxy", containerOptions.Proxy },
                    { "https_proxy", containerOptions.Proxy },
                    { "no_proxy", containerOptions.NoProxy },
                    { "HTTP_PROXY", containerOptions.Proxy },
                    { "HTTPS_PROXY", containerOptions.Proxy },
                    { "NO_PROXY", containerOptions.NoProxy },
                    { "COLUMNS", containerOptions.ShellColumns },
                    { "PYTHONUNBUFFERED", "true" }, // python実行時の標準出力・エラーのバッファリングをなくす
                    { "LC_ALL", "C.UTF-8" },        // python実行時のエラー回避
                    { "LANG", "C.UTF-8" }  // python実行時のエラー回避
                },
                EntryPoint = preprocessHistory.Preprocess.EntryPoint,

                ClusterManagerToken = token,
                RegistryTokenName   = registryMap.RegistryTokenKey,
                IsNodePort          = true
            };

            // 前処理はGitの未指定も許可するため、その判定
            if (preprocessHistory.Preprocess.RepositoryGitId != null)
            {
                long gitId = preprocessHistory.Preprocess.RepositoryGitId == -1 ?
                             CurrentUserInfo.SelectedTenant.DefaultGitId.Value : preprocessHistory.Preprocess.RepositoryGitId.Value;

                var gitEndpoint = gitLogic.GetPullUrl(preprocessHistory.Preprocess.RepositoryGitId.Value, preprocessHistory.Preprocess.RepositoryName, preprocessHistory.Preprocess.RepositoryOwner);
                if (gitEndpoint != null)
                {
                    inputModel.EnvList.Add("MODEL_REPOSITORY", gitEndpoint.FullUrl);
                    inputModel.EnvList.Add("MODEL_REPOSITORY_URL", gitEndpoint.Url);
                    inputModel.EnvList.Add("MODEL_REPOSITORY_TOKEN", gitEndpoint.Token);
                }
                else
                {
                    //Git情報は必須にしているので、無ければエラー
                    return(Result <ContainerInfo, string> .CreateErrorResult("Git credential is not valid."));
                }
            }

            // ユーザの任意追加環境変数をマージする
            AddUserEnvToInputModel(preprocessHistory.OptionDic, inputModel);

            //使用できるノードを取得し、制約に追加
            inputModel.ConstraintList = new Dictionary <string, List <string> >()
            {
                { containerOptions.ContainerLabelHostName, GetAccessibleNode() }
            };

            if (string.IsNullOrEmpty(preprocessHistory.Partition) == false)
            {
                // パーティション指定があれば追加
                inputModel.ConstraintList.Add(containerOptions.ContainerLabelPartition, new List <string> {
                    preprocessHistory.Partition
                });
            }

            var outModel = await clusterManagementService.RunContainerAsync(inputModel);

            if (outModel.IsSuccess == false)
            {
                return(Result <ContainerInfo, string> .CreateErrorResult(outModel.Error));
            }
            return(Result <ContainerInfo, string> .CreateResult(new ContainerInfo()
            {
                Name = outModel.Value.Name,
                Status = outModel.Value.Status,
                Host = outModel.Value.Host,
                Configuration = outModel.Value.Configuration
            }));
        }