diff --git a/docker/deployment/Dockerfile b/docker/deployment/Dockerfile index 6a198707..8e05a4ae 100644 --- a/docker/deployment/Dockerfile +++ b/docker/deployment/Dockerfile @@ -8,9 +8,6 @@ COPY init_portainer.go / RUN CGO_ENABLED=0 go build -o init_portainer /init_portainer.go RUN chmod +x /init_portainer -COPY endpoint.go / -RUN CGO_ENABLED=0 go build -o endpoint /endpoint.go -RUN chmod +x /endpoint # step2: Copy build go program to portainer # Dockerfile refer to: https://github.com/portainer/portainer/blob/develop/build/linux/Dockerfile @@ -18,6 +15,5 @@ FROM portainer/portainer-ce:2.20.1 LABEL maintainer="websoft9" LABEL version="2.20.1" COPY --from=builder /init_portainer / -COPY --from=builder /endpoint / -ENTRYPOINT ["/init_portainer"] +ENTRYPOINT ["/init_portainer"] \ No newline at end of file diff --git a/docker/deployment/README.md b/docker/deployment/README.md index d7ec8142..0695bb32 100644 --- a/docker/deployment/README.md +++ b/docker/deployment/README.md @@ -1,3 +1,6 @@ # Readme -- create local endpoint and lock +From official Portainer image, and: + +- Initialize username and password +- Initialize the local environment endpoint diff --git a/docker/deployment/endpoint.go b/docker/deployment/endpoint.go deleted file mode 100644 index 653bb0da..00000000 --- a/docker/deployment/endpoint.go +++ /dev/null @@ -1,167 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "net/url" - "strings" - "os" - "time" -) - -const ( - AdminUser = "admin" - EndpointURL = "http://localhost:9000/api/endpoints" - AuthURL = "http://localhost:9000/api/auth" - CredentialLoc = "/data/credential" -) - -type Endpoint struct { - Name string `json:"name"` - URL string `json:"url"` -} - -type EndpointCreation struct { - Name string `json:"name"` - EndpointCreationType int `json:"EndpointCreationType"` -} - -type AuthResponse struct { - Jwt string `json:"jwt"` -} - -type Credentials struct { - Username string `json:"username"` - Password string `json:"password"` -} - -func main() { - - fmt.Println("Start to create endpoint...") - client := &http.Client{} - - password, err := getPassword() - if err != nil { - fmt.Println("Failed to get password:", err) - return - } - - token, err := authenticate(client, AdminUser, password) - if err != nil { - fmt.Println("Failed to authenticate:", err) - return - } - - endpoints, err := queryEndpoints(client, token) - if err != nil { - fmt.Println("Failed to query endpoints:", err) - return - } - - for _, endpoint := range endpoints { - if endpoint.Name == "local" && endpoint.URL == "unix:///var/run/docker.sock" { - fmt.Println("Endpoint exists, exiting...") - return - } - } - - fmt.Println("Endpoint does not exist, creating...") - createEndpoint(client, token) - - fmt.Println("Endpoint created successfully") -} - -func getPassword() (string, error) { - for { - if _, err := os.Stat(CredentialLoc); os.IsNotExist(err) { - fmt.Printf("%s does not exist, waiting for 3 seconds...\n", CredentialLoc) - time.Sleep(3 * time.Second) - } else { - fmt.Printf("%s exists, proceeding...\n", CredentialLoc) - data, err := ioutil.ReadFile(CredentialLoc) - if err != nil { - return "", err - } - return string(data), nil - } - } -} - -func authenticate(client *http.Client, username, password string) (string, error) { - credentials := Credentials{Username: username, Password: password} - credentialsJson, err := json.Marshal(credentials) - if err != nil { - return "", err - } - - req, err := http.NewRequest("POST", AuthURL, bytes.NewBuffer(credentialsJson)) - req.Header.Set("Content-Type", "application/json") - resp, err := client.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return "", err - } - - var authResponse AuthResponse - err = json.Unmarshal(body, &authResponse) - if err != nil { - return "", err - } - - return authResponse.Jwt, nil -} - -func queryEndpoints(client *http.Client, token string) ([]Endpoint, error) { - req, err := http.NewRequest("GET", EndpointURL, nil) - req.Header.Set("Authorization", "Bearer "+token) - resp, err := client.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - var endpoints []Endpoint - err = json.Unmarshal(body, &endpoints) - if err != nil { - return nil, err - } - - return endpoints, nil -} - -func createEndpoint(client *http.Client, token string) error { - data := url.Values{ - "Name": {"local"}, - "EndpointCreationType": {"1"}, - } - - req, err := http.NewRequest("POST", EndpointURL, strings.NewReader(data.Encode())) - if err != nil { - return err - } - - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - req.Header.Set("Authorization", "Bearer "+token) - - resp, err := client.Do(req) - - if resp.StatusCode != http.StatusCreated { - body, _ := ioutil.ReadAll(resp.Body) - return fmt.Errorf("Failed to create endpoint: %s, Response body: %s", resp.Status, string(body)) - } - - return nil -} \ No newline at end of file diff --git a/docker/deployment/init_portainer.go b/docker/deployment/init_portainer.go index 8e2bece1..31a82159 100644 --- a/docker/deployment/init_portainer.go +++ b/docker/deployment/init_portainer.go @@ -1,70 +1,238 @@ package main import ( - "fmt" - "io/ioutil" - "math/rand" - "os" - "os/exec" - "time" + "bytes" + "crypto/rand" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "math/big" + "net/http" + "net/url" + "os" + "os/exec" + "strings" + "time" ) +const ( + portainerURL = "http://localhost:9000/api" + maxRetries = 5 + retryDelay = 5 * time.Second + charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@$()_" + credentialFilePath = "/data/credential" + initFlagFilePath = "/data/init.flag" +) + +type Credentials struct { + Username string `json:"username"` + Password string `json:"password"` +} + func main() { - - filePath := "/data/credential" - initPath := "/data/init" + // 检查初始化标志文件是否存在 + if _, err := os.Stat(initFlagFilePath); err == nil { + log.Println("Initialization has already been completed by another instance.") + startPortainer(os.Args[1:]...) + return + } - _, err := os.Stat(filePath) - if os.IsNotExist(err) { + // 启动 Portainer + startPortainer(os.Args[1:]...) - _, err := os.Stat(initPath) + // 等待 Portainer 启动 + waitForPortainer() - if os.IsNotExist(err) { - fmt.Println("credential is not exist, create it.") - password := generatePassword(16) - err := writeToFile(filePath, password) - if err != nil { - fmt.Println("write file error:", err) - return - } - } else { - fmt.Println("credential is exist, skip it.") - } - - // call portainer - cmd := exec.Command("./portainer", "--admin-password-file", filePath, "--hide-label", "owner=websoft9") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - err = cmd.Run() - if err != nil { - fmt.Println("error running compiled_program:", err) - return - }else{ - os.Create(initPath) - } - }else{ - fmt.Println("credential is exist, skip it.") - cmd := exec.Command("./portainer", "--hide-label", "owner=websoft9") - cmd.Run() - } + // 初始化 Portainer + adminUsername := "admin" + adminPassword := generateRandomPassword(12) + if err := initializePortainerUser(adminUsername, adminPassword); err != nil { + log.Fatalf("Failed to initialize Portainer user: %v", err) + } else { + if err := writeCredentialsToFile(adminPassword); err != nil { + log.Fatalf("Failed to write credentials to file: %v", err) + } else { + if err := initializeLocalEndpoint(adminUsername, adminPassword); err != nil { + log.Fatalf("Failed to initialize local endpoint: %v", err) + } else { + fmt.Println("Portainer initialization completed successfully.") + // 创建初始化标志文件 + if err := ioutil.WriteFile(initFlagFilePath, []byte("initialized"), 0644); err != nil { + log.Fatalf("Failed to create initialization flag file: %v", err) + } + } + } + } } -func generatePassword(length int) string { - rand.Seed(time.Now().UnixNano()) +func startPortainer(args ...string) { + cmd := exec.Command("/portainer", args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr - charset := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@$()_" + if err := cmd.Start(); err != nil { + log.Fatalf("Failed to start Portainer: %v", err) + } - password := make([]byte, length) - for i := range password { - password[i] = charset[rand.Intn(len(charset))] - } - - return string(password) + // 等待 Portainer 进程结束 + if err := cmd.Wait(); err != nil { + log.Fatalf("Portainer process exited with error: %v", err) + } } -func writeToFile(filePath , content string) error { +func waitForPortainer() { + timeout := time.Duration(60) * time.Second + start := time.Now() - return ioutil.WriteFile(filePath , []byte(content), 0755) -} \ No newline at end of file + for { + resp, err := http.Get(portainerURL + "/system/status") + if err == nil && resp.StatusCode == http.StatusOK { + fmt.Println("Portainer is up!") + break + } + + if time.Since(start) > timeout { + fmt.Println("Timeout waiting for Portainer") + os.Exit(1) + } + + fmt.Println("Waiting for Portainer...") + time.Sleep(2 * time.Second) + } +} + +func generateRandomPassword(length int) string { + password := make([]byte, length) + for i := range password { + char, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) + password[i] = charset[char.Int64()] + } + return string(password) +} + +func initializePortainerUser(username, password string) error { + requestBody := Credentials{Username: username, Password: password} + jsonBody, err := json.Marshal(requestBody) + if err != nil { + return fmt.Errorf("error marshaling request body: %w", err) + } + + resp, err := retryRequest("POST", portainerURL+"/users/admin/init", "application/json", bytes.NewBuffer(jsonBody)) + if err != nil { + return fmt.Errorf("error making request to Portainer API: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusConflict { + return nil + } + + if resp.StatusCode != http.StatusOK { + body, _ := ioutil.ReadAll(resp.Body) + return fmt.Errorf("unexpected response status: %d, body: %s", resp.StatusCode, body) + } + + return nil +} + +func initializeLocalEndpoint(username, password string) error { + authBody := Credentials{Username: username, Password: password} + jsonBody, err := json.Marshal(authBody) + if err != nil { + return fmt.Errorf("error marshaling auth body: %w", err) + } + + resp, err := retryRequest("POST", portainerURL+"/auth", "application/json", bytes.NewBuffer(jsonBody)) + if err != nil { + return fmt.Errorf("error authenticating with Portainer API: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := ioutil.ReadAll(resp.Body) + return fmt.Errorf("unexpected response status: %d, body: %s", resp.StatusCode, body) + } + + var authResult map[string]string + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("error reading authentication response: %w", err) + } + + err = json.Unmarshal(body, &authResult) + if err != nil { + return fmt.Errorf("error parsing authentication response: %w", err) + } + + jwtToken := authResult["jwt"] + + endpointBody := url.Values{} + endpointBody.Set("Name", "local") + endpointBody.Set("EndpointCreationType", "1") + + req, err := http.NewRequest("POST", portainerURL+"/endpoints", strings.NewReader(endpointBody.Encode())) + if err != nil { + return fmt.Errorf("error creating endpoint request: %w", err) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Authorization", "Bearer "+jwtToken) + + client := &http.Client{} + resp, err = client.Do(req) + if err != nil { + return fmt.Errorf("error creating endpoint in Portainer API: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusConflict { + body, _ := ioutil.ReadAll(resp.Body) + return fmt.Errorf("unexpected response status: %d, body: %s", resp.StatusCode, body) + } + + if resp.StatusCode == http.StatusConflict { + fmt.Println("Endpoint already exists, but this is considered a success.") + } else { + fmt.Println("Endpoint created successfully.") + } + + return nil +} + +func writeCredentialsToFile(password string) error { + err := ioutil.WriteFile(credentialFilePath, []byte(password), 0755) + if err != nil { + return fmt.Errorf("error writing password to file: %w", err) + } + + return nil +} + +func retryRequest(method, url, contentType string, body *bytes.Buffer) (*http.Response, error) { + client := &http.Client{} + for i := 0; i < maxRetries; i++ { + var req *http.Request + var err error + + if body != nil { + req, err = http.NewRequest(method, url, bytes.NewBuffer(body.Bytes())) + } else { + req, err = http.NewRequest(method, url, nil) + } + + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Content-Type", contentType) + + resp, err := client.Do(req) + if err == nil { + return resp, nil + } + + log.Printf("Request failed: %v. Retrying in %v...", err, retryDelay) + time.Sleep(retryDelay) + } + return nil, fmt.Errorf("max retries reached") +} diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 22d37285..071a550e 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -25,11 +25,7 @@ services: - /data/compose:/data/compose - /var/run/docker.sock:/var/run/docker.sock #- /run/podman/podman.sock:/var/run/docker.sock - healthcheck: - test: ["CMD", "/endpoint"] - interval: 10s - timeout: 30s - retries: 4 + command: ["--hide-label", "owner=websoft9"] labels: - "owner=websoft9" - "com.docker.compose.w9_http.port=9000" diff --git a/docker/proxy/Dockerfile b/docker/proxy/Dockerfile index a04a39b8..be7fc178 100644 --- a/docker/proxy/Dockerfile +++ b/docker/proxy/Dockerfile @@ -2,18 +2,16 @@ # from Dockerfile: https://github.com/NginxProxyManager/nginx-proxy-manager/blob/develop/docker/Dockerfile # from image: https://hub.docker.com/r/jc21/nginx-proxy-manager -FROM jc21/nginx-proxy-manager:2.11.1 +FROM jc21/nginx-proxy-manager:2.11.3 LABEL maintainer="Websoft9" -LABEL version="2.11.1" +LABEL version="2.11.3" -RUN apt-get update && apt-get install --no-install-recommends -y curl jq && rm -rf /var/lib/apt/lists/* -COPY ./config/initproxy.conf /etc/ -COPY ./s6/w9init/setuser.sh /app/setuser.sh -COPY ./s6/w9init/migration.sh /app/migration.sh -COPY ./s6/w9init/setproxy.sh /app/setproxy.sh -RUN chmod +x /app/setuser.sh /app/migration.sh /app/setproxy.sh +COPY ./config/initproxy.conf /data/nginx/default_host/initproxy.conf +COPY ./init_nginx.sh /app/init_nginx.sh +RUN chmod +x /app/init_nginx.sh +# 修复nginx启动加载ip_ranges失败的问题 RUN export add_ip_data="const ipDataFile={[CLOUDFRONT_URL]:'ip-ranges.json',[CLOUDFARE_V4_URL]:'ips-v4',[CLOUDFARE_V6_URL]:'ips-v6'}[url];logger.info(ipDataFile);if(ipDataFile){return fs.readFile(__dirname+'/../lib/ipData/'+ipDataFile,'utf8',(error,data)=>{if(error){logger.error('fetch '+ipDataFile+' error');reject(error);return}logger.info('fetch '+ipDataFile+' success');resolve(data)})}" && \ sed -i "s#url);#&${add_ip_data}#g" /app/internal/ip_ranges.js && \ mkdir -p /app/lib/ipData && cd /app/lib/ipData && \ @@ -21,4 +19,15 @@ RUN export add_ip_data="const ipDataFile={[CLOUDFRONT_URL]:'ip-ranges.json',[CLO curl -O https://www.cloudflare.com/ips-v4 && \ curl -O https://www.cloudflare.com/ips-v6 -CMD ["/bin/sh", "-c", "/app/migration.sh && /app/setuser.sh && /app/setproxy.sh && tail -f /dev/null"] + +# 为所有nginx的代理统一加入websockets支持 +RUN proxy_line=("proxy_set_header Upgrade \$http_upgrade;" "proxy_set_header Connection upgrade;") && \ + proxy_path="/etc/nginx/conf.d/include/proxy.conf" && \ + length=${#proxy_line[@]} && \ + for ((i=0; i<$length; i++)); do \ + if ! grep -Fxq "${proxy_line[$i]}" $proxy_path; then \ + echo "${proxy_line[$i]}" >> $proxy_path; \ + fi; \ + done + +ENTRYPOINT [ "/app/init_nginx.sh" ] \ No newline at end of file diff --git a/docker/proxy/README.md b/docker/proxy/README.md index ca8ba684..ea1ce338 100644 --- a/docker/proxy/README.md +++ b/docker/proxy/README.md @@ -1,7 +1,6 @@ # Readme -From official Nginx Proxy Manager image, and: +From official Nginx Proxy Manager image, and: -- add init_proxy.conf to image -- init install wizard and modify user and password -- lock the line of BoltDB at Portainer where envrionment=1 +- Copy the initproxy.conf file to the nginx directory to initialize the custom configuration +- Initialize username and password through environment variables diff --git a/docker/proxy/init_nginx.sh b/docker/proxy/init_nginx.sh new file mode 100644 index 00000000..6200c372 --- /dev/null +++ b/docker/proxy/init_nginx.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# 设置密码目录 +credential_path="/data/credential" + +# 检查是否已经存在密码文件 +if [ ! -f "$credential_path" ]; then + # 设置用户名和生成随机密码 + INITIAL_ADMIN_EMAIL="admin@mydomain.com" + INITIAL_ADMIN_PASSWORD=$(openssl rand -base64 16 | tr -d '/+' | cut -c1-16) + + # 设置环境变量 + export INITIAL_ADMIN_EMAIL + export INITIAL_ADMIN_PASSWORD + + # 写入密码文件 + mkdir -p "$(dirname "$credential_path")" + credential_json="{\"username\":\"$INITIAL_ADMIN_EMAIL\",\"password\":\"$INITIAL_ADMIN_PASSWORD\"}" + echo "$credential_json" > "$credential_path" +else + # 从密码文件中读取用户名和密码 + INITIAL_ADMIN_EMAIL=$(jq -r '.username' "$credential_path") + INITIAL_ADMIN_PASSWORD=$(jq -r '.password' "$credential_path") + + # 设置环境变量 + export INITIAL_ADMIN_EMAIL + export INITIAL_ADMIN_PASSWORD +fi + +# 启动 Nginx +exec /init diff --git a/docker/proxy/s6/README.md b/docker/proxy/s6/README.md deleted file mode 100644 index a94cc964..00000000 --- a/docker/proxy/s6/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# S6 - -S6 is a mulitply process management tools at Nginx Proxy Manager. - -- nginx_proxy() at migration.sh: Migration initproxy.conf to Nginx, condition is compare Container created time and Named Volumes created time diff --git a/docker/proxy/s6/w9init/migration.sh b/docker/proxy/s6/w9init/migration.sh deleted file mode 100644 index d82024e2..00000000 --- a/docker/proxy/s6/w9init/migration.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash - -set +e - -nginx_proxy(){ - - current_time=$(date +%s) - shadow_modified_time=$(stat -c %Y /etc/shadow) - time_difference=$((current_time - shadow_modified_time)) - - if [ ! -f /data/nginx/proxy_host/initproxy.conf ] || [ $time_difference -le 60 ] - then - cp /etc/initproxy.conf /data/nginx/proxy_host/ - echo "Update initproxy.conf to Nginx" - else - echo "Don't need to update initproxy.conf to Nginx" - fi - -} - -nginx_proxy - -set -e \ No newline at end of file diff --git a/docker/proxy/s6/w9init/setproxy.sh b/docker/proxy/s6/w9init/setproxy.sh deleted file mode 100644 index dd1d5c17..00000000 --- a/docker/proxy/s6/w9init/setproxy.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash - -set +e - -# Define the array -proxy_line=("proxy_set_header Upgrade \$http_upgrade;" "proxy_set_header Connection upgrade;") - -# Define the file path -proxy_path="/etc/nginx/conf.d/include/proxy.conf" - -# Get the length of the array -length=${#proxy_line[@]} - -# Loop over the array and append each item to the file -for ((i=0; i<$length; i++)); do - # Check if the line already exists in the file - if ! grep -Fxq "${proxy_line[$i]}" $proxy_path; then - # If the line does not exist in the file, append it - echo "${proxy_line[$i]}" >> $proxy_path - fi -done - - -set -e \ No newline at end of file diff --git a/docker/proxy/s6/w9init/setuser.sh b/docker/proxy/s6/w9init/setuser.sh deleted file mode 100644 index 70d146b3..00000000 --- a/docker/proxy/s6/w9init/setuser.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/bin/bash - -set +e -username="admin@mydomain.com" -password=$(openssl rand -base64 16 | tr -d '/+' | cut -c1-16) -token="" -cred_path="/data/credential" -max_attempts=10 - -echo "Start to change nginxproxymanage users" -if [ -e "$cred_path" ]; then - echo "File $cred_path exists. Exiting script." - exit 0 -fi - -echo "create diretory" -mkdir -p "$(dirname "$cred_path")" - -sleep 10 -while [ -z "$token" ]; do - sleep 5 - login_data=$(curl -X POST -H "Content-Type: application/json" -d '{"identity":"admin@example.com","scope":"user", "secret":"changeme"}' http://localhost:81/api/tokens) - token=$(echo $login_data | jq -r '.token') -done - -echo "Change username(email)" -for attempt in $(seq 1 $max_attempts); do - response=$(curl -X PUT -H "Content-Type: application/json" -H "Authorization: Bearer $token" -d '{"email": "'$username'", "nickname": "admin", "is_disabled": false, "roles": ["admin"]}' http://localhost:81/api/users/1) - if [ $? -eq 0 ]; then - echo "Set username successful" - break - else - echo "Set username failed, retrying..." - sleep 5 - if [ $attempt -eq $max_attempts ]; then - echo "Failed to set username after $max_attempts attempts. Exiting." - exit 1 - fi - fi -done - -echo "Update password" -for attempt in $(seq 1 $max_attempts); do - response=$(curl -X PUT -H "Content-Type: application/json" -H "Authorization: Bearer $token" -d '{"type":"password","current":"changeme","secret":"'$password'"}' http://localhost:81/api/users/1/auth) - if [ $? -eq 0 ]; then - echo "Set password successful" - echo "Save to credential" - json="{\"username\":\"$username\",\"password\":\"$password\"}" - echo "$json" > "$cred_path" - break - else - echo "Set password failed, retrying..." - sleep 5 - if [ $attempt -eq $max_attempts ]; then - echo "Failed to set password after $max_attempts attempts. Exiting." - exit 1 - fi - fi -done - -set -e