improve micro service

This commit is contained in:
qiaofeng1227 2024-07-25 08:39:06 +08:00
parent 8a6366724f
commit 63582ea263
12 changed files with 279 additions and 357 deletions

View file

@ -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<help@websoft9.com>"
LABEL version="2.20.1"
COPY --from=builder /init_portainer /
COPY --from=builder /endpoint /
ENTRYPOINT ["/init_portainer"]

View file

@ -1,3 +1,6 @@
# Readme
- create local endpoint and lock
From official Portainer image, and:
- Initialize username and password
- Initialize the local environment endpoint

View file

@ -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
}

View file

@ -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() {
// 检查初始化标志文件是否存在
if _, err := os.Stat(initFlagFilePath); err == nil {
log.Println("Initialization has already been completed by another instance.")
startPortainer(os.Args[1:]...)
return
}
filePath := "/data/credential"
initPath := "/data/init"
// 启动 Portainer
startPortainer(os.Args[1:]...)
_, err := os.Stat(filePath)
if os.IsNotExist(err) {
// 等待 Portainer 启动
waitForPortainer()
_, err := os.Stat(initPath)
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)
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")
}

View file

@ -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"

View file

@ -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<help@websoft9.com>"
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" ]

View file

@ -2,6 +2,5 @@
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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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